Skip to content
ohsudo, Home
Linux

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.

Beginner3 min readApplies to npm 8+ · Node.js 18+ · Linux · macOS · nvm · Docker

by OhSudo ·

npm error code EACCES
npm error syscall mkdir
npm error path /usr/local/lib/node_modules/typescript
npm error errno -13
npm 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

Terminal window
# Global installs: use nvm, so they live in your home
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.41.0/install.sh | bash
nvm install --lts
# Project or cache files owned by root: take them back
sudo chown -R "$(id -u):$(id -g)" node_modules ~/.npm

1. 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:

Terminal window
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.41.0/install.sh | bash
# open a new terminal
nvm install --lts
npm install -g typescript

Or change npm’s global folder, if you keep the system’s Node.js:

Terminal window
mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
npm install -g typescript

On 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:

Terminal window
ls -ld node_modules ~/.npm
find node_modules ~/.npm ! -user "$USER" | head

Give them back:

Terminal window
sudo chown -R "$(id -u):$(id -g)" node_modules ~/.npm

With package-lock.json in place, you can also delete and reinstall, as your own user this time:

Terminal window
sudo rm -rf node_modules
npm ci

Warning

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-alpine
WORKDIR /app # created as root
USER node
COPY 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-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["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.

Tools

Try it yourself

Articles

Related articles