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 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.
# 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 lockfileCommon 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.
sudo chown -R "$(id -u):$(id -g)" "$HOME/.npm" node_modules 2>/dev/null || true
npm ciAvoid root-owned state and sudo installs
- Do not run
sudo npm install- it leaves root-owned files behind. - Use a user-writable global prefix (
npm config set prefix ~/.npm-global) ornpxinstead of global installs. - 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.
# 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') }}-v2How 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.