npm "EACCES: permission denied": fix it without sudo
Fix npm EACCES permission denied on global installs and in projects: use a version manager or your own prefix, repair root-owned files, and avoid sudo.
by OhSudo ·
npm error code EACCESnpm error syscall mkdirnpm error path /usr/local/lib/node_modules/typescriptnpm error errno -13npm error Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/typescript'Older npm versions write npm ERR! instead of npm error. EACCES means your user isn’t allowed to write to that path. The line that matters is path: it tells you which of the two usual cases you have.
| The path is in | The case | Go to |
|---|---|---|
/usr/local/lib/node_modules, /usr/lib/node_modules |
A global install, into a folder owned by root | Section 1 |
Your project, ~/.npm or ~/.config |
Files owned by root, because someone ran sudo npm earlier |
Section 2 |
/app or similar, in a container |
The image’s user can’t write there | Section 3 |
TL;DR
# Global installs: use nvm, so they live in your homecurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.41.0/install.sh | bashnvm install --lts
# Project or cache files owned by root: take them backsudo chown -R "$(id -u):$(id -g)" node_modules ~/.npm1. Global installs: don’t use sudo
The tempting fix is sudo npm install -g. It works, and it’s how the second case below starts. It also runs the install scripts of every package and its dependencies as root. Instead, make global installs live somewhere you own.
A version manager is the best fix. nvm, fnm or Volta install Node.js in your home directory, so global packages need no special rights, and you can switch versions per project:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.41.0/install.sh | bash# open a new terminalnvm install --ltsnpm install -g typescriptOr change npm’s global folder, if you keep the system’s Node.js:
mkdir -p ~/.npm-globalnpm config set prefix ~/.npm-globalecho 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrcsource ~/.bashrcnpm install -g typescriptOn macOS the default shell is zsh, so use ~/.zshrc. And many tools don’t need a global install at all: npx typescript --version runs one without installing it for good.
2. Files owned by root in your project or cache
Someone ran sudo npm install once, and now root owns some files, so your normal user can’t change them. See who owns what:
ls -ld node_modules ~/.npmfind node_modules ~/.npm ! -user "$USER" | headGive them back:
sudo chown -R "$(id -u):$(id -g)" node_modules ~/.npmWith package-lock.json in place, you can also delete and reinstall, as your own user this time:
sudo rm -rf node_modulesnpm ciWarning
Don’t chmod -R 777. It makes the error go away by letting every user and every process on the machine change those files, and everything you run later from them. Don’t do it to /usr/local either, and don’t chown all of /usr: other programs depend on those permissions. To see what a mode really allows, try the chmod Calculator. If the error isn’t about npm at all, see Permission denied when running a script.
3. In Docker
The classic Dockerfile mistake: switching to the node user before it has a folder it can write to:
FROM node:24-alpineWORKDIR /app # created as rootUSER nodeCOPY package*.json ./RUN npm ci # EACCES: mkdir '/app/node_modules'Install as root, and switch at the end. Your code stays read-only for the app, which is what you want:
FROM node:24-alpineWORKDIR /appCOPY package*.json ./RUN npm ci --omit=devCOPY . .USER nodeCMD ["node", "server.js"]The Dockerfile Generator writes it this way. If the app itself needs to write somewhere, create that one folder and chown it to node. A bind mount (-v .:/app) keeps the host’s owners, so compare your host’s user ID with the container’s: id -u on the host, and docker compose exec app id -u.
Shell, we have a problem!
Similar, but different: EPERM: operation not permitted is common on Windows, where an antivirus or an open editor holds the file: close them, and try again. ERESOLVE unable to resolve dependency tree is a version conflict: see that article. EEXIST means a file is already there, which npm install -g --force may overwrite, after you’ve read what it is. ENOSPC is a full disk: see No space left on device. And if the error names /var/run/docker.sock, see Docker permission denied.
Found this useful? Share it.
Found a mistake? Let us know at contact@ohsudo.com.