Skip to content
Latchkey

npm EACCES "permission denied" on the npm cache - Fix in CI

EACCES on the npm cache or node_modules means the user running npm cannot write where it needs to. In CI and Docker this is almost always a directory owned by root from an earlier step.

What this error means

npm fails while writing to ~/.npm/_cacache or node_modules with "EACCES: permission denied". It frequently appears after a step ran as root (or a cached layer was created as root) and a later step runs as a non-root user.

npm output
npm error code EACCES
npm error syscall mkdir
npm error path /home/runner/.npm/_cacache/...
npm error errno -13
npm error Error: EACCES: permission denied, mkdir '/home/runner/.npm/_cacache/...'

Diagnose it: reproduce the CI install locally

Install failures are usually environment drift rather than a broken lockfile: a different package-manager major, a different Node version, or a cache that is being restored from a run with different inputs. Reproduce the CI conditions before changing the lockfile, because regenerating it hides the real cause.

Terminal
# match the runner exactly, then install from a clean slate
node --version && npm --version
rm -rf node_modules
npm ci --foreground-scripts

# if that succeeds locally but fails in CI, the difference is the cache
# or the package-manager version, not your lockfile

Common causes

Cache or node_modules owned by root

A previous step (often a Docker build run as root, or a sudo npm call) created ~/.npm or node_modules owned by root. When the build user runs npm next, it cannot write there.

Installing globally without permission

npm install -g writes to a system prefix the CI user cannot modify. Global installs as non-root commonly hit EACCES.

How to fix it

Fix ownership of the cache and modules

Hand the npm directories back to the current user.

Terminal
sudo chown -R "$(id -u):$(id -g)" "$HOME/.npm" node_modules 2>/dev/null || true
npm ci

Avoid root-owned state and sudo installs

  1. Do not run sudo npm install - it leaves root-owned files behind.
  2. Use a user-writable global prefix (npm config set prefix ~/.npm-global) or npx instead of global installs.
  3. In Dockerfiles, run npm as the same user that runs the build, not root.

Verify the fix survives a cold cache

A green run immediately after a fix often proves nothing, because it restored a cache written before the change. Force a cold install once to confirm the fix is real.

.github/workflows/ci.yml
# temporarily bust the cache key to prove the fix on a cold runner
- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: npm
    cache-dependency-path: package-lock.json
# then bump this suffix once, run, and remove it
#   key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}-v2

How to prevent it

  • Never mix root and non-root npm runs in the same workspace.
  • Set a user-writable npm prefix for global tools.
  • Bake correct ownership into base images.

Frequently asked questions

What causes npm EACCES "permission denied" on the npm cache?
There are 2 common causes: cache or node_modules owned by root and installing globally without permission. A previous step (often a Docker build run as root, or a sudo npm call) created ~/.npm or node_modules owned by root.
How do I fix npm EACCES "permission denied" on the npm cache?
There are 2 fixes depending on which cause you have: fix ownership of the cache and modules and avoid root-owned state and sudo installs. Work through them in order, since the first is the most common.
What does npm EACCES "permission denied" on the npm cache actually mean?
npm fails while writing to ~/.npm/_cacache or node_modules with "EACCES: permission denied".
How do I stop npm EACCES "permission denied" on the npm cache happening again?
Never mix root and non-root npm runs in the same workspace. The prevention section lists 3 changes that keep it from recurring.
Can Latchkey fix this automatically?
Yes. Latchkey runs your GitHub Actions on managed runners that detect this failure, apply the fix, and retry the job automatically - self-healing is on by default.

Related guides

References

This is a setup failure, not a bug in your code. Latchkey detects, repairs, and retries it for you. Start free → 30-day trial · No credit card