Fix: EACCES permission denied when installing npm packages globally
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix 'Error: EACCES: permission denied, access /usr/local/lib/node_modules' when running npm install -g on macOS or Linux. Multiple solutions ranked by recommendation.
The Error
You try to install an npm package globally and get:
npm ERR! Error: EACCES: permission denied, access '/usr/local/lib/node_modules'Variations of this error include:
npm ERR! Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/some-package'
npm ERR! Error: EACCES: permission denied, access '/usr/local/bin'
npm ERR! Error: EACCES: permission denied, rename '/usr/local/lib/node_modules/some-package'The command that triggers it is usually something like:
npm install -g typescript
npm install -g create-react-app
npm install -g yarnWhy This Happens
When you install Node.js from the official installer, a package manager like Homebrew, or your distro’s default repositories, the global node_modules directory ends up in a system-owned location — typically /usr/local/lib/node_modules on macOS or /usr/lib/node_modules on some Linux distributions.
These directories are owned by root. Your regular user account does not have write access to them. When npm tries to write files there during a global install, the operating system blocks it.
This is not an npm bug. It is standard Unix permission enforcement. The real problem is that Node was installed in a way that requires root privileges for global packages.
macOS vs Linux
- macOS: This commonly happens after installing Node from the official
.pkginstaller or via Homebrew. The global prefix is usually/usr/local, which on modern macOS may be owned by root even if Homebrew previously made it user-writable. - Linux: This happens with distro-packaged Node (
apt install nodejs,dnf install nodejs) because the prefix/usror/usr/localis always root-owned. It also happens with the official NodeSource repositories. For general Linux permission issues, see also Fix: bash permission denied.
npm Version History: Why Global Install Behavior Keeps Changing
The set of things that even count as a “global install” has shifted across npm major versions, and treating them all the same is a common source of confusion.
npm 5 (May 2017) introduced package-lock.json and npx. Once npx shipped, the strongest reason for global installs (one-off CLI usage) largely disappeared. Despite this, many tutorials still tell readers to npm install -g for tools that ship a bin field — which is rarely the right advice anymore.
npm 7 (October 2020) made peerDependencies install automatically for the first time and introduced workspaces. Both changes mean a global install now pulls down many more transitive packages than it did under npm 6, multiplying the risk window if you grant the install root via sudo. npm 7 also tightened how the prefix config is resolved: npm config set prefix writes to your user .npmrc, but /etc/npmrc and a project-local .npmrc still override it. If your prefix change “does not stick,” check all three layers with npm config list -l.
npm 9 (October 2022) removed several legacy install behaviors. Most importantly, npm install no longer reports “shrinkwrap” warnings, the --global-style flag was reworked, and node-gyp is bundled at a newer version that fails earlier on permission issues — which is why some users only started seeing EACCES after upgrading to npm 9 even though their setup had not changed. npm 10 (shipped with Node 20) and npm 11 continued this direction and reduced reliance on sudo-driven setup paths.
A note on prefix history: Before Node 16, the default prefix on macOS Homebrew installs was sometimes user-writable. Homebrew later tightened permissions on /usr/local/lib. If npm install -g used to work on the same machine and now fails, an OS or Homebrew upgrade is the most likely cause — not anything you did. The cleanest path forward is still nvm (Fix 1), which sidesteps the system prefix entirely.
Fix 1: Use nvm (Node Version Manager) — Recommended
This is the best long-term solution. nvm installs Node in your home directory (~/.nvm/), so every global install is user-owned. No permission issues, ever.
Step 1: Uninstall your current system Node (optional but clean)
macOS (Homebrew):
brew uninstall nodeLinux (Debian/Ubuntu):
sudo apt remove nodejs npmLinux (Fedora/RHEL):
sudo dnf remove nodejs npmIf you installed from the official .pkg on macOS, you can leave it — nvm will override it via your shell PATH.
Step 2: Install nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bashClose and reopen your terminal, or reload your shell config:
source ~/.bashrc # bash
source ~/.zshrc # zshStep 3: Install Node through nvm
nvm install --ltsStep 4: Verify
which node
# Should show something like: /home/youruser/.nvm/versions/node/v22.x.x/bin/node
npm install -g typescript
# Works without sudo or permission errorsWhy this works: nvm places Node and its global node_modules inside ~/.nvm/, which your user fully owns. There is never a reason to use sudo with nvm-managed Node.
Why this matters: Running
sudo npm install -gis dangerous because npm packages can execute arbitrary code during install via lifecycle scripts. A compromised or malicious package running as root can modify system files, install backdoors, or delete critical data. Never give npm root privileges.
Fix 2: Change npm’s Default Directory
If you don’t want to use nvm, you can tell npm to store global packages in a directory you own.
Step 1: Create a directory for global packages
mkdir -p ~/.npm-globalStep 2: Configure npm to use it
npm config set prefix '~/.npm-global'Step 3: Add the new directory to your PATH
Add this line to your shell config file (~/.bashrc, ~/.zshrc, or ~/.profile):
export PATH="$HOME/.npm-global/bin:$PATH"Then reload:
source ~/.bashrc # or ~/.zshrcStep 4: Test it
npm install -g typescript
tsc --versionNote: This only fixes npm global installs. If you also use npx or corepack, make sure they respect the same prefix. If you’re hitting dependency resolution errors instead, see Fix: npm ERR! ERESOLVE unable to resolve dependency tree. You can verify your current prefix with:
npm config get prefix
# Should show: /home/youruser/.npm-globalFix 3: Use npx Instead of Global Install
Many tools that people install globally don’t actually need to be installed globally. npx (bundled with npm 5.2+) runs a package without installing it:
# Instead of:
npm install -g create-react-app
create-react-app my-app
# Do this:
npx create-react-app my-app# Instead of:
npm install -g typescript
tsc file.ts
# Do this:
npx tsc file.tsThis avoids the permission problem entirely because nothing is written to the global directory. The package is temporarily cached in a user-owned location.
When npx is NOT a replacement: If you need a CLI tool available everywhere at all times (like nodemon, pm2, or http-server), a proper global install via Fix 1 or Fix 2 is better.
Fix 4: Fix Permissions with chown
You can change ownership of the global directory to your user:
sudo chown -R $(whoami) /usr/local/lib/node_modules
sudo chown -R $(whoami) /usr/local/bin
sudo chown -R $(whoami) /usr/local/shareOn some Linux systems the path may differ:
sudo chown -R $(whoami) /usr/lib/node_modulesThis works but has downsides:
- It changes ownership of shared system directories, which can cause problems if other software (like Homebrew on macOS) also writes there.
- OS updates or other package managers may reset these permissions.
- It is a workaround, not a root-cause fix. Fix 1 or Fix 2 are cleaner.
Why You Should Never Use sudo npm install -g
You may see advice online suggesting:
# DO NOT DO THIS
sudo npm install -g some-packageThis is dangerous for several reasons:
- npm runs arbitrary scripts. Packages can define
preinstall,postinstall, and other lifecycle scripts. Running them as root means a malicious or compromised package can do anything to your system — delete files, install backdoors, modify system configs. - File ownership becomes a mess. Some files in your
~/.npmcache end up owned by root, which causes further permission errors for non-sudo installs later. You end up needing sudo for everything. - It masks the real problem. The correct fix is to not require root privileges for Node development tooling at all.
If you have already run sudo npm install -g and now have mixed ownership in your cache, clean it up:
sudo chown -R $(whoami) ~/.npmThen apply Fix 1 or Fix 2 to prevent the problem going forward.
Pro Tip: After setting up nvm, verify with
which nodethat it shows a path inside~/.nvm/. If it still shows/usr/local/bin/node, your shell is loading the system Node before nvm. Make sure the nvm sourcing lines are at the end of your~/.bashrcor~/.zshrc, after any other PATH modifications.
Still Not Working?
nvm is installed but you still get EACCES
This usually means your shell is still using the system Node instead of the nvm-managed one.
Check which Node is active:
which nodeIf it shows /usr/local/bin/node or /usr/bin/node instead of a path inside ~/.nvm/, nvm is not loaded in your current session. Verify nvm is sourced in your shell config:
# This block should be in your ~/.bashrc or ~/.zshrc:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"Then open a new terminal and run:
nvm use --ltsIf you are using a shell other than bash or zsh (like fish), note that nvm does not support fish natively. Use nvm.fish or bass instead.
Corporate machine with restricted permissions
On managed workstations where you cannot install nvm or change system directories:
- Use Fix 2 (change npm prefix) — this only requires write access to your home directory, which is almost always available.
- Use npx for one-off tool usage.
- If your home directory is also restricted (network-mounted home with quotas), ask your IT team to set up a local writable directory and point npm’s prefix there:
npm config set prefix '/opt/local/youruser/npm-global'- On some corporate setups, Node is installed via tools like Volta or asdf instead of nvm. These work the same way — they install Node per-user. Check if your company already has one configured.
Error persists after changing prefix
If you changed the prefix with npm config set prefix but still get EACCES, check for a conflicting global .npmrc:
npm config list
npm config get prefixThere may be a system-level config file at /etc/npmrc or a project-level .npmrc overriding your user setting. You can check all config sources:
npm config list -lLook for the prefix value and confirm it points to your user-owned directory.
EACCES on ~/.npm (cache directory)
A different but related error:
npm ERR! Error: EACCES: permission denied, mkdir '/home/user/.npm/_cacache'This typically happens because you previously ran npm with sudo, and now root owns parts of the cache. Fix it with:
sudo chown -R $(whoami) ~/.npmA package’s postinstall script writes outside its own directory
Some packages (notably native-binary downloaders and certain analytics tools) try to write to /usr/local/include or /opt during postinstall. Even if your prefix is fine, EACCES can come from these scripts touching system paths. Inspect the log to see which path the package is trying to write, then either pin to an older version that did not have the script or disable scripts globally for installs you trust to be benign:
npm install --ignore-scripts <pkg>Warning: --ignore-scripts skips legitimate setup too. Only use it when you understand the package and are willing to run its scripts manually afterward.
Docker container EACCES on bind-mounted volume
If you run npm install -g inside a container with the host directory bind-mounted at /usr/src/app, the host UID may not match the container’s node user. The fix is to chown the volume in the Dockerfile or run the container with --user "$(id -u):$(id -g)". Both this and the host-permission case are UID mismatches across a permission boundary.
Volta or asdf is overriding your prefix
If you installed Volta or asdf later, they each manage their own shims and may shadow your ~/.npm-global/bin. Run which npm and which node to confirm which manager owns the binaries. If you intended to use nvm but which node returns a path under ~/.volta or ~/.asdf, uninstall the other manager or remove its sourcing block from your shell config. See also Fix: npm peer dep conflict for related issues that surface only after a Node version change.
Related: If you’re seeing dependency resolution errors instead of permission errors, see Fix: npm ERR! ERESOLVE unable to resolve dependency tree. For Node-side resolution errors after fixing permissions, see Fix: Cannot find module in Node.js.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: EACCES: permission denied, mkdir / open / unlink (Node.js)
How to fix EACCES permission denied errors in Node.js for mkdir, open, unlink, and scandir operations, covering npm global installs, Docker, NVM, CI/CD, and filesystem permissions.
Fix: bash: ./script.sh: Permission denied (Linux, macOS, WSL)
How to fix 'Permission denied' errors in bash, including chmod +x for scripts, file ownership with chown, EACCES in Node.js, Docker volume permissions, SELinux, ACLs, and WSL issues.
Fix: pnpm Peer Dependency Errors (Missing or Incompatible Peer Dependencies)
How to fix pnpm peer dependency errors — why pnpm is stricter than npm about peer deps, how to resolve missing peers, configure peerDependencyRules, and handle incompatible version ranges.
Fix: EMFILE Too Many Open Files / ulimit Error on Linux
How to fix EMFILE too many open files errors on Linux and Node.js — caused by low ulimit file descriptor limits, file handle leaks, and how to increase limits permanently.