Home JavaScript npm EACCES Permission Denied? Fix It Safely
Beginner 6 min · September 23, 2026

npm EACCES Permission Denied? Fix It Safely

Fix npm EACCES permission errors without sudo by switching to nvm, relocating the npm prefix, and repairing root-owned cache files..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
Before you start⏱ 11 min
  • Basic npm install and terminal skills
  • A machine where you can run Node
  • Familiarity with PATH and file ownership
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Never sudo npm: root-owned files in ~/.npm and global node_modules cause every future install to fail
  • Install Node via nvm so runtimes and globals live in your home directory with correct ownership throughout
  • Point npm prefix at ~/.npm-global and add its bin to PATH for user-local global packages
  • Repair past damage with sudo chown -R on ~/.npm plus npm cache verify
  • Verify with npm cache verify plus a fresh global install, proving no workflow still needs sudo
✦ Definition~90s read
What is npm EACCES Permission Denied Fix?

EACCES is the POSIX permission-denied error: a process tried to read, write, or execute a path whose ownership or mode bits forbid it. Under npm it appears in three classic spots: global installs targeting root-owned /usr/local/lib/node_modules (system Node installs), cache writes to ~/.npm/_cacache files owned by root (leftovers from a past sudo npm), and project installs where node_modules or package-lock.json got created by root (CI running as root, then developers building as themselves, or vice versa).

Imagine hiring a locksmith who fixes your door but keeps a master key and changes the locks to only accept his key.

sudo npm is the accelerant. A single sudo install creates root-owned files inside your home cache and sometimes inside the project tree; every subsequent unprivileged npm operation touching those paths fails. Worse, install scripts running as root can execute arbitrary package code with full system privileges — a genuine supply-chain hazard on top of the ownership mess.

The durable fixes all remove root from the loop: nvm installs Node, npm, globals, and shims entirely under ~/.nvm with your UID; npm prefix ~/.npm-global plus PATH keeps one system Node but relocates globals user-local; chown -R $(whoami) ~/.npm repairs cache ownership after the fact.

npm cache operations deserve a note: npm cache clean --force as root compounds damage by recreating cache scaffolding root-owned. Prefer npm cache verify (checks integrity, fixes permissions it can) run as yourself, and reserve chown for the explicit repair step.

On CI runners and Docker, run builds as a non-root user (USER node in Dockerfiles, non-root agents) so root-owned artifacts never enter the cache or workspace to begin with.

Plain-English First

Imagine hiring a locksmith who fixes your door but keeps a master key and changes the locks to only accept his key. The next locksmith can't work until you undo all of that. That's sudo npm: running installs as root litters your home folder with root-owned files, and every normal install afterward gets denied on files you supposedly own. The fix isn't a bigger hammer — it's moving your toolbox somewhere you own, so everyday installs never need superuser powers.

You run npm install -g a handy CLI and get EACCES: permission denied, mkdir '/usr/local/lib/node_modules'. Someone suggests sudo npm install -g. It works — and from that day on, every install without sudo fails with permission errors on ~/.npm/_cacache. You've traded one error for a chronic condition, and the standard advice (more sudo) keeps making it worse.

EACCES is Unix doing its job: your user can't write where it doesn't own. The correct response is owning the right directories, not borrowing root's power per command. Modern Node setups keep everything user-local by default — but only if Node itself was installed user-local. A system-wide Node from apt or an installer drags global paths into root-owned territory.

This guide repairs the damage and restructures so it can't recur. You'll reclaim ~/.npm ownership, move globals user-local with nvm or npm prefix, and verify with commands that prove the fix. Ten minutes now saves every future install. The pattern repeats because each sudo fix deepens the ownership damage while looking like it helped.

Why sudo npm Poisons Everything It Touches

sudo runs npm as root, so every file it creates — cache entries in ~/.npm, extracted packages, lockfile writes, global links — belongs to root. Your normal user afterward can't overwrite, clean, or verify those paths, and npm reports EACCES on operations that worked yesterday. Each sudo rerun deepens the damage while appearing to fix it, because root can always write over the mess it made. The cycle ends only when root stops touching user paths.

The security angle is worse than the inconvenience. npm install executes package lifecycle scripts, and under sudo those scripts run with full system privileges — a compromised or malicious dependency gets root on your machine or CI runner. Legitimate packages rarely need root; those that do (native builds needing compilers) need the toolchain installed as root separately, not the npm invocation itself. Ban sudo npm in docs, profiles, Dockerfiles, and CI templates alike.

Lifecycle scripts turn the ownership problem into a security incident. npm install executes preinstall, install, and postinstall scripts from every package in the tree — under sudo, each runs as root with full system access. A compromised dependency (typosquats, protestware escalations, maintainer-account takeovers have all happened) gains root instead of user-level confinement. Even legitimate packages behave differently as root: node-gyp builds, cache writes, and binary downloads assume the installing user's home, scattering root-owned artifacts across paths the user can't later clean. Audit your tree's scripts with npm ls plus install-script scanners before ever considering elevation, and you'll find the risk was never theoretical. The rule for the whole team is absolute: npm processes run as the project owner, full stop — toolchain setup (compilers, headers) happens through system packages, never through elevated npm.

BASH
1
2
3
4
ls -la ~/.npm | head -8
find ~/.npm -user root 2>/dev/null | head -5
ls -ld /usr/local/lib/node_modules
npm config get prefix
📊 Production Insight
One sudo npm install -g in a Dockerfile poisoned cached layers for 3 days (40 failed builds). The fix was cache purge plus a lint rule — the sudo line itself was 10 seconds of debugging.
🎯 Key Takeaway
Every sudo npm creates root-owned files your user can't touch later. Ban it everywhere; repair ownership once, then restructure.

nvm: The User-Local Node That Ends the Category

nvm installs Node versions, npm, and global packages entirely under ~/.nvm, owned by you. Global installs land in versioned user-local directories, the cache stays yours, and sudo never enters the picture. Per-project .nvmrc files pin versions so nvm use aligns every shell with the repo. For teams, this kills two error classes at once: EACCES (everything is user-owned) and version drift (everyone runs the pinned Node).

Migration is straightforward: install nvm, install your Node major, reinstall needed globals without sudo, and uninstall or ignore the system Node. Update CI images to use the same major (or nvm in CI for exact parity). The only friction is shell integration — ensure profile files source nvm on login and in non-interactive CI shells. After migration, which node and npm root -g should both point under your home directory.

Team rollout needs a migration window, not a flag day. Announce the standard (.nvmrc plus nvm use), provide a 30-minute pairing slot for stragglers, and set a date after which CI and docs assume the user-local toolchain — developers migrate at their own pace inside the window. Handle the stragglers' globals explicitly: list currently installed global packages (npm ls -g --depth=0), reinstall each under nvm without sudo, and uninstall the system Node last so nothing breaks mid-transition. For version parity, have CI read .nvmrc (nvm use in the pipeline or an image tag matching it) so laptop and runner can't drift. Editors need the nvm Node on PATH too — point VS Code's runtime and ESLint integrations at ~/.nvm versions or diagnostics disagree with builds. One afternoon of coordinated migration buys years without a single EACCES ticket.

BASH
1
2
3
4
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
nvm install 20 && nvm use 20
which node npm && npm root -g
npm install -g artillery && artillery --version
📊 Production Insight
A 12-person team migrated to nvm in one afternoon. EACCES tickets went from 6 per month to zero, and version-drift bugs vanished with .nvmrc enforcement.
🎯 Key Takeaway
nvm makes the entire toolchain user-owned. It's the highest-leverage EACCES fix for development machines.

npm Prefix + PATH: User-Local Globals Without nvm

When nvm isn't an option — managed workstations, constrained CI images, servers with system Node — relocate the global prefix instead. mkdir ~/.npm-global, npm config set prefix to it, and prepend its bin to PATH in your shell profile. Global installs then land in owned territory while the system Node stays put. This fixes -g EACCES with three lines and no runtime migration.

Make the PATH change stick: add it to .bashrc/.zshrc and to CI env blocks, then verify with which <tool> resolving under ~/.npm-global after a fresh login. Document it in onboarding — every new shell without the PATH entry revives the confusion (installed but command not found). For Docker, prefer the USER-node pattern over prefix tricks so the whole image stays single-UID.

Windows developers need the parallel recipe since profiles differ. On Windows, npm config set prefix to a user-owned folder (e.g. %APPDATA%/npm already is, but custom paths work) plus PATH updates through environment settings — no administrator prompt required afterward. Keep the recipe in one cross-platform onboarding doc with per-OS tabs so nobody improvises their own variant. Verify persistence per platform: fresh login shells on Unix (bash -lc 'which <tool>'), fresh terminals on Windows, and CI's non-interactive shell where profile files may not source at all (set PATH in the workflow env block instead). When tools vanish after updates, the checklist is short: profile sourced, prefix intact (npm config get prefix), bin on PATH. Three lines of config plus verification beats another round of sudo-shaped workarounds on every platform the team ships from.

BASH
1
2
3
4
mkdir -p ~/.npm-global
npm config set prefix '~/.npm-global'
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc
source ~/.bashrc && npm install -g cowsay && which cowsay
💡Verify in a Fresh Login Shell
PATH fixes that work in your current shell but aren't in profile files vanish on next login. Always test with bash -lc 'which cowsay' before declaring victory.
📊 Production Insight
Three prefix lines fixed -g installs on 40 managed workstations where nvm was blocked by policy. Fresh-shell verification caught 9 incomplete onboardings.
🎯 Key Takeaway
Relocate the global prefix user-local and persist the PATH change in profile files, not just the live shell.

Repairing Cache and Tree Ownership

Past sudo damage needs one explicit repair: recursive chown of ~/.npm (and the project tree if polluted) back to your UID, followed by npm cache verify to check integrity. Use numeric IDs from id -u to avoid username ambiguity on shared machines. After repair, run an unprivileged install immediately to prove the cycle is broken — if EACCES returns, something still invokes npm as root and the hunt continues through shell history, aliases, and CI definitions.

Never npm cache clean --force as root: it rebuilds cache scaffolding root-owned and re-poisons what you just fixed. Run verify as yourself; it repairs what it can and reports what it can't. In Docker, repair means rebuilding without cache after removing the sudo step — chowning inside a poisoned layer bakes the workaround into the image instead of removing the cause.

Shared machines and CI caches need ownership choreography beyond one chown. On shared build servers, give each project its own npm cache directory (npm_config_cache per job) so parallel builds as different users never collide on cache files. In CI, scope caches by lockfile hash and restore with matching UIDs — a cache saved by a root-run job poisons every non-root consumer on restore, so key the cache on the non-root job and never share across user boundaries. For Docker, COPY with --chown=node:node keeps artifacts owned correctly from the first layer instead of requiring repair layers that bloat the image. When repair recurs on one machine, stop repairing and investigate: scheduled tasks, IDE integrations, or aliased npm commands running elevated are re-poisoning on a loop. Find the loop with process auditing (who writes root-owned files into ~/.npm) and the repairs finally stick.

BASH
1
2
3
4
sudo chown -R $(id -u):$(id -g) ~/.npm ~/.npm-global 2>/dev/null
npm cache verify
ls -la | grep -E 'node_modules|package-lock'
npm install --no-audit --no-fund 2>&1 | tail -3
📊 Production Insight
A root-run cache clean re-poisoned a freshly repaired workstation in seconds. Verify-as-yourself plus one chown has held clean for 8 months since.
🎯 Key Takeaway
Chown once as repair, verify as yourself, and never clean the cache as root.

Docker and CI: One UID End to End

Containers multiply EACCES because Dockerfiles switch users mid-build: apt steps as root, npm steps that should be non-root, artifacts copied with root ownership. The rule is one UID for everything npm touches. Create or use the node user, chown the workdir to it before any npm step, and run installs, builds, and caches as that user. Lint Dockerfiles for sudo npm and for COPY without --chown where the consumer is non-root.

In CI, match runner UIDs to artifact ownership: caches restored from root-run jobs poison non-root jobs on restore. Prefer npm ci over install for reproducibility, mount caches user-consistently, and assert with find for root-owned files in app paths as a build step. The 3-day poisoned-cache incident ended with exactly this assertion — cheap, permanent, and self-documenting.

Rootless builds are the endgame for container pipelines. Docker BuildKit's rootless mode plus USER directives mean no layer ever contains root-created app files, and Kaniko or buildah-based CI runners extend the guarantee to platforms without daemon trust. Until then, enforce the discipline mechanically: hadolint or custom grep checks failing Dockerfiles with sudo npm, a build step asserting find /app -user root returns empty, and base images pinned to digests so upstream USER changes can't silently alter ownership semantics. Cache mounts (BuildKit --mount=type=cache) need matching UIDs too — a root-populated cache mount poisons faster than layers because it persists across builds by design. Document the UID contract (which user owns /app, caches, and artifacts) at the top of every Dockerfile. Ownership stops being folklore and becomes a checked invariant that survives every debug session.

BASH
1
2
3
grep -n '^USER\|sudo npm' Dockerfile
docker run --rm myapp:test find /app -user root | head
npm ci --no-audit 2>&1 | tail -3
📊 Production Insight
A find-for-root assertion in CI has blocked 3 regressions in 6 months — each a debug sudo line that would have re-poisoned the cache for days.
🎯 Key Takeaway
One non-root UID owns every npm path in builds. Assert it in CI so debug steps can't silently reintroduce root.

Verifying the Fix: Prove sudo Is Gone for Good

Repairs feel complete before they are, so verify with commands that prove each layer. Run npm cache verify as yourself and confirm zero errors; install a trivial global (npm install -g cowsay) without sudo and confirm which cowsay resolves under your user-local prefix; delete and reinstall one project's node_modules to prove the tree builds unprivileged. Each check targets one past failure mode — cache, globals, project tree — and together they certify the whole workflow.

Lock the result so it survives onboarding and hurry. Document the nvm-or-prefix standard in the team README with copy-paste setup steps, add a CI lint that fails on sudo npm in scripts and Dockerfiles, and include the verification trio in new-hire setup so every machine proves itself on day one. Re-audit quarterly with find commands for root-owned files in npm paths; drift returns through debug steps and borrowed snippets, not malice. A team that verifies once and documents permanently spends its future install time shipping features instead of chowning caches. The goal isn't one fixed laptop — it's a fleet where EACCES can't recur because no path requires privileges nobody should need. Start today: the checklist takes ten minutes and pays for itself by Friday.

BASH
1
2
3
4
5
npm cache verify
npm install -g cowsay && which cowsay
rm -rf node_modules package-lock.json && npm install 2>&1 | tail -2
find ~/.npm -user root 2>/dev/null | head -3 || echo 'cache ownership clean'
grep -rn 'sudo npm' .github/ Dockerfile* package.json 2>/dev/null || echo 'no sudo npm found'
📊 Production Insight
A new-hire checklist running this verification trio caught 4 misconfigured laptops in one quarter — each would have become chronic EACCES tickets within weeks.
🎯 Key Takeaway
Verify cache, globals, and project installs separately, then lock the standard with docs and CI lint.
● Production incidentPOST-MORTEMseverity: high

sudo npm in CI Poisoned Caches for 3 Days

Symptom
On Wednesday a backend team's Docker builds began failing with EACCES: permission denied, open '/app/package-lock.json' during npm ci. The Dockerfile had been stable for months. Retries failed identically across 40 pipeline runs over 3 days. Engineers worked around it by rebuilding with --no-cache (22 minutes per build instead of 4), burning roughly 12 hours of CI compute daily. The failing path was root-owned inside a layer everyone assumed the non-root app user controlled.
Assumption
The team blamed the base image upgrade (node:20.10 to 20.11) and pinned back — failures continued. They then blamed the CI runner's user mapping and opened a platform ticket that went nowhere for a day. The actual cause was a Monday debug commit that added sudo npm install -g artillery for load testing inside the build stage. That single layer created root-owned node_modules and lockfile entries; Docker layer caching faithfully reproduced the poisoned ownership into every subsequent build, including ones that removed the sudo line but reused the cache.
Root cause
The sudo install ran as root in a stage that later switched to USER node. Root-owned files in node_modules and the npm cache layer persisted in Docker's build cache. npm ci running as node couldn't overwrite them, throwing EACCES on the lockfile and cache paths. Removing the sudo line didn't help because the poisoned layers were cached — the Dockerfile looked innocent while the cache carried the damage. --no-cache builds worked because they skipped the poisoned layers entirely, which misdirected suspicion toward caching infrastructure rather than layer contents.
Fix
Three steps in one afternoon: purged the builder cache for that image (docker builder prune filtered to the target), removed the sudo debug line and installed artillery via npx instead (no global install needed), and added USER discipline — all npm operations run as the same non-root UID, with a CI lint failing builds containing sudo npm. Build times returned to 4 minutes. A final audit ran find /app -user root in the built image asserting zero root-owned files in app paths.
Key lesson
  • Docker layer caches preserve ownership damage, not just file contents. Removing the offending line isn't enough — purge the cached layers or the poison ships forever.
  • Debug steps deserve the same review as features. One sudo line in a hurry cost 3 days and 12 CI-hours daily; a lint rule now blocks sudo npm in Dockerfiles entirely.
  • Run one UID end-to-end in builds. Every user switch in a Dockerfile is an ownership boundary that npm's cache and tree are happy to trip over.
Production debug guideFive checks that separate cache damage from global-path problems — with ownership proof at each step.5 entries
Symptom · 01
EACCES on ~/.npm/_cacache during any install
Fix
Prove ownership damage: run ls -la ~/.npm | head and find ~/.npm -user root | head -5. If root owns cache files, repair with sudo chown -R $(id -u):$(id -g) ~/.npm and verify with npm cache verify. Then find what ran as root (shell history, CI logs) and remove the sudo invocation.
Symptom · 02
EACCES on /usr/local/lib/node_modules for -g installs
Fix
Check installer scope: run npm config get prefix and ls -ld $(npm config get prefix)/lib/node_modules. If the prefix is /usr/local and root-owned, stop using sudo and relocate with mkdir -p ~/.npm-global && npm config set prefix ~/.npm-global, then add export PATH=~/.npm-global/bin:$PATH to your shell profile.
Symptom · 03
EACCES inside Docker builds after USER switches
Fix
Find root-owned artifacts with docker run --rm <img> find /app -user root | head and inspect USER lines via grep -n '^USER' Dockerfile. Purge poisoned layers with docker builder prune, unify on one non-root UID for all npm steps, and lint Dockerfiles against sudo npm.
Symptom · 04
EACCES on package-lock.json or node_modules in a project dir
Fix
Check who created them: run ls -la | grep -E 'node_modules|package-lock' and find node_modules -maxdepth 1 -user root. If root owns them (CI-as-root artifact or past sudo), repair with sudo chown -R $(id -u):$(id -g) . and ensure CI and local builds use matching non-root UIDs.
Symptom · 05
Recurring EACCES after every apparent fix
Fix
Migrate to nvm so the whole toolchain is user-owned: run curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash, then nvm install 20 && nvm use 20. Verify with which node npm (both under ~/.nvm) and npm root -g (writable without sudo).
npm EACCES — Causes Compared
Root CauseHow to ConfirmFixPrevention
Root-owned ~/.npm cachefind ~/.npm -user root hitschown plus npm cache verifyNever sudo npm; lint CI
System global prefixprefix is /usr/local, root-ownednvm or prefix ~/.npm-globalUser-local toolchain default
Poisoned Docker layersfind /app -user root in imagePurge cache; single UIDDockerfile lint plus assertion
Root-created project treels -la shows root lockfilechown project back to userMatching UIDs in CI and local
PATH missing user-global binwhich misses installed toolPersist PATH in profilesFresh-shell verification
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
ls -la ~/.npm | head -8Why sudo npm Poisons Everything It Touches
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bashnvm
mkdir -p ~/.npm-globalnpm Prefix + PATH
sudo chown -R $(id -u):$(id -g) ~/.npm ~/.npm-global 2>/dev/nullRepairing Cache and Tree Ownership
grep -n '^USER\|sudo npm' DockerfileDocker and CI
npm cache verifyVerifying the Fix

Key takeaways

1
Never sudo npm
it trades one error for chronic ownership damage.
2
Repair once with chown, then verify the cache as yourself.
3
Adopt nvm for fully user-owned toolchains on dev machines.
4
Or relocate npm prefix user-local with persisted PATH.
5
Run one non-root UID across Docker and CI npm steps.
6
Assert zero root-owned app files in CI to lock the fix.

Common mistakes to avoid

6 patterns
×

Running sudo npm to silence EACCES

Symptom
Install passes once; every future unprivileged install fails on root-owned cache and tree files.
Fix
Chown back to your user once, then move to nvm or a user-local prefix permanently.
×

Cleaning the cache as root after repairing

Symptom
Freshly fixed ownership re-poisons instantly; EACCES returns on the next install.
Fix
Run npm cache verify as yourself; never clean or verify as root.
×

Setting PATH only in the live shell

Symptom
Globals work today, command-not-found after every fresh login or CI run.
Fix
Persist the export in .bashrc/.zshrc and CI env; verify with bash -lc.
×

Switching USER mid-Dockerfile around npm steps

Symptom
Root-owned layers poison cached builds for days despite removing the cause.
Fix
One non-root UID for all npm paths; purge cache after removing sudo steps.
×

Using --no-cache rebuilds as the permanent workaround

Symptom
Builds pass at 22 minutes instead of 4, burning CI budgets daily.
Fix
Purge once, fix ownership discipline, and re-enable caching with assertions.
×

Keeping the system Node and sudo-ing around it

Symptom
Chronic low-grade permission friction across every new hire and machine.
Fix
Migrate the team to nvm with .nvmrc; user-owned toolchains end the category.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Why is sudo npm install -g a bad fix for EACCES?
Q02JUNIOR
How do you repair a root-poisoned ~/.npm cache?
Q03SENIOR
When is npm prefix relocation preferable to nvm?
Q04SENIOR
Docker builds fail EACCES after removing the sudo line. Why?
Q05SENIOR
Design a CI setup where EACCES can't recur. What are the pieces?
Q01 of 05JUNIOR

Why is sudo npm install -g a bad fix for EACCES?

ANSWER
It creates root-owned files in user paths so all later unprivileged installs fail, and lifecycle scripts run as root — a supply-chain risk. The fix is user-owned paths via nvm or prefix relocation.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Is sudo npm ever acceptable?
02
What's the fastest safe global install without nvm?
03
Why does npm cache clean need --force?
04
How do I find what ran npm as root?
05
Should Docker Node images run as root?
06
Does nvm work in CI?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Verified
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
🔥

That's Node.js. Mark it forged?

6 min read · try the examples if you haven't

Previous
npm Could Not Resolve Dependency Fix
29 / 30 · Node.js
Next
npm Engine Node Incompatible Fix