You run a global install, for example npm install -g some-cli, and npm stops with this (npm 10.9.8 on Node v22.23.2; reproduced with an unwritable prefix, path shown as it appears with the common /usr/local prefix, stack frames trimmed):
npm error code EACCES
npm error syscall mkdir
npm error path /usr/local/lib/node_modules/some-cli
npm error errno -13
npm error Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/some-cli'
npm error at async mkdir (node:internal/fs/promises:858:10)
npm error ...
npm error
npm error The operation was rejected by your operating system.
npm error It is likely you do not have the permissions to access this file as the current user
npm error
npm error If you believe this might be a permissions issue, please double-check the
npm error permissions of the file and its containing directories, or try running
npm error the command again as root/Administrator.Older npm versions print the same fields with an npm ERR! prefix, and the syscall may be access, mkdir, rename or symlink depending on which file operation failed first. The meaning: npm tried to write into its global install directory, and the operating system said your user does not own it.
Quick fix checklist
- Run
npm config get prefix. If it prints/usr/localor/usr, global installs need root, and that is the actual problem. - Best fix: install Node through a version manager (nvm, fnm or Volta), which keeps everything in your home directory.
- Quick fix without reinstalling Node:
npm config set prefix ~/.localand add~/.local/bintoPATH. - Do you need a global install at all?
npx some-cliruns a tool without installing it globally. - If the path in the error is under
~/.npm, an earliersudo npmleft root-owned cache files; run thechowncommand npm prints. - In Docker, the user running
npmmust own the directory it writes (COPY --chown=node:node).
Before you start
You need a macOS or Linux shell and a few minutes to edit your shell profile (~/.zprofile for zsh, ~/.profile or ~/.bashrc for bash). On Windows, the default global prefix is under your user profile (%AppData%\npm), so this error is rare there; Windows permission failures are more often EPERM caused by a file locked by an editor or antivirus. Commands below assume Node 22 or 24 LTS and npm 10 or later.
Why it happens
A global install writes to two places under npm’s prefix: the package goes into <prefix>/lib/node_modules, and its executables are linked into <prefix>/bin, which is on your PATH. Where the prefix points depends on how Node was installed. The installer from nodejs.org and many Linux packages put Node in /usr/local or /usr, which are owned by root, and npm’s prefix defaults to the directory Node was installed in. So the first global install as a normal user hits a root-owned directory and the kernel returns EACCES (errno 13, “permission denied”).
Local installs (npm install inside a project) write to ./node_modules, which you own, so they work fine with the same setup. That is the tell: EACCES only on -g is a prefix problem.
There is a second variant. If you ever ran sudo npm ..., npm may have written files owned by root into your cache at ~/.npm. Later, ordinary non-global installs fail with EACCES on a path inside ~/.npm/_cacache, and npm 10 recognises that case and prints a specific message:
npm error Your cache folder contains root-owned files, due to a bug in
npm error previous versions of npm which has since been addressed.
npm error
npm error To permanently fix this problem, please run:
npm error sudo chown -R 501:20 "/Users/you/.npm"The numbers are your user and group IDs. Running that one command is correct and safe because it only touches your own cache.
Step-by-step walkthrough
Step 1: Confirm where npm is writing
npm config get prefix
ls -ld "$(npm config get prefix)/lib/node_modules"
which node npmIf the prefix is /usr/local and the directory is owned by root, you have found the cause. If the prefix is under your home directory (for example ~/.nvm/versions/node/v22.23.2), global installs should already work, and an EACCES there means something inside it became root-owned, almost always from an earlier sudo.
Step 2: Choose the long-term fix (version manager)
A version manager installs Node per user, so the prefix lives in your home directory and you can switch versions per project. Any of these works:
- nvm (macOS/Linux): install it from its README, then
nvm install --lts. - fnm (macOS/Linux/Windows, fast, written in Rust):
fnm install --lts, thenfnm use --lts. - Volta (pins tool versions per project):
volta install node@22.
After installing, open a new shell and check that which node points into your home directory and npm config get prefix follows it. Global installs then need no special permissions. You do not need to uninstall the system Node first, but remove any prefix= line you added to ~/.npmrc earlier, or it will override the version manager’s prefix.
Step 3: Or move the global prefix into your home directory
If you cannot change how Node is installed (for example on a managed work laptop), point npm’s prefix at a directory you own. This is the approach npm’s own documentation describes:
npm config set prefix ~/.local
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.profile
source ~/.profileFor zsh on macOS, add the PATH line to ~/.zprofile (or source ~/.profile from it). Now a global install lands in ~/.local/lib/node_modules and its command in ~/.local/bin:
npm install -g some-cli
which some-cli/Users/you/.local/bin/some-cliPackages you previously installed globally under /usr/local stay there; reinstall the ones you still use under the new prefix.
Step 4: Ask whether it should be global at all
Most global installs are habits from older tutorials. Project tools (TypeScript, ESLint, Prettier, test runners) belong in devDependencies, so every developer and CI job uses the same version, and you run them through npm scripts or npx, which finds the local copy first:
npm install --save-dev typescript
npx tsc --versionScaffolding tools you run once per project do not need installing at all: npx create-vite@latest my-app (or npm create vite@latest my-app) downloads into npm’s cache and runs it. Keep true globals for tools you use across all projects.
Step 5: Fix ownership inside Docker images
The official node images run as root by default and provide an unprivileged node user (uid 1000). EACCES appears after you add USER node for security but the files were copied as root, so npm ci cannot create node_modules:
npm error Error: EACCES: permission denied, mkdir '/app/node_modules'Give the node user ownership of what it writes:
FROM node:22-slim
WORKDIR /app
RUN chown node:node /app
USER node
COPY --chown=node:node package*.json ./
RUN npm ci --omit=dev
COPY --chown=node:node . .
CMD ["node", "server.js"]If the image genuinely needs a global CLI as the node user, the docker-node best practices set ENV NPM_CONFIG_PREFIX=/home/node/.npm-global and add /home/node/.npm-global/bin to PATH, which is the container version of Step 3.
Worked scenario
A developer on Ubuntu installed Node from the NodeSource apt repository. npm install -g @angular/cli failed with EACCES under /usr/lib/node_modules, so they ran sudo npm install -g @angular/cli. It worked. A week later, a plain npm install in an unrelated project failed with the “cache folder contains root-owned files” message, and ng update failed with EACCES when it tried to write.
Diagnosis: npm config get prefix printed /usr, explaining the first failure. find ~/.npm -user root | head listed cache entries owned by root, created during the sudo run, explaining the second.
Fix, in order:
sudo chown -R "$(id -u):$(id -g)" ~/.npm
sudo npm uninstall -g @angular/cli
npm config set prefix ~/.local
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.profile && source ~/.profile
npm install -g @angular/cliThe first command repairs only the user’s cache. The sudo npm uninstall is the one legitimate sudo use here: removing what an earlier sudo put in a root-owned location. Everything afterward runs as the user. A later move to fnm removed the need for the custom prefix altogether.
Common mistake
sudo npm install -g is the fix that appears to work, and npm’s own error text even mentions running as root. It causes three problems:
- Install scripts can run as root. Packages and their dependencies may define
preinstall,installandpostinstallscripts. Undersudo, a compromised or careless dependency gets root access to your machine. - It creates root-owned files where your user works, such as
~/.npm, which produce new EACCES errors later in commands that never usedsudo. - It may use a different Node.
sudoresetsPATHon many systems, so with a version manager you getsudo: npm: command not found, or a system Node you did not intend to use.
The other tempting fix is sudo chown -R $USER /usr/local (or /usr/lib/node_modules). It changes ownership of directories other software relies on, can break package managers that expect root ownership there, and you will repeat it after every Node upgrade. Changing the prefix or using a version manager solves it once, without touching system directories.
Verify the behavior
npm config get prefix
npm install -g some-cli
ls -l "$(which some-cli)"
find "$(npm config get prefix)" ~/.npm -user root 2>/dev/null | headExpect a prefix under your home directory, an install that succeeds without sudo, an executable owned by your user, and no output from the find command. In Docker, build the image and run docker run --rm <image> id; it should report uid=1000(node), and the container should start without permission errors.
Interview exercise
“A colleague says the standard fix for npm EACCES is to always use sudo. How would you respond, and what would you set up for a team so this does not come up?”
Answer and reasoning
EACCES on a global install means npm’s prefix points to a root-owned directory, so the error is about where npm writes, not about npm needing more privilege. sudo makes the error disappear by running the whole install, including every dependency’s lifecycle scripts, as root, which is a real supply-chain risk. It also leaves root-owned files in the user’s cache and prefix, so it creates the next permission error.
For a team I would standardise on a version manager (nvm, fnm or Volta) with a .nvmrc or Volta pin in each repository, so everyone gets the same Node version installed in their home directory. Project tools go in devDependencies and run via npm scripts or npx, so very little needs a global install, and those versions are reviewed in pull requests. Container images run as the non-root node user with --chown on copied files. That turns a recurring support question into a setup step that is correct by default, and it follows least privilege on both laptops and servers.
Continue learning
- Node.js interview questions and Node.js MCQs
- Node.js environment variables and configuration
- npm ERESOLVE: unable to resolve dependency tree
- Error: Cannot find module in Node.js
- Official reference: Resolving EACCES permissions errors when installing packages globally, Downloading and installing Node.js and npm, npx and the docker-node best practices