The situation
You're setting up a new Mac or onboarding onto a project, you run a routine npm install -g, and get smacked with this:
npm ERR! code EACCES
npm ERR! syscall access
npm ERR! path /usr/local/lib/node_modules
npm ERR! errno -13
npm ERR!
npm ERR! Error: EACCES: permission denied, access '/usr/local/lib/node_modules'
npm ERR! [Error: EACCES: permission denied, access '/usr/local/lib/node_modules'] {
npm ERR! errno: -13,
npm ERR! code: 'EACCES',
npm ERR! syscall: 'access',
npm ERR! path: '/usr/local/lib/node_modules'
npm ERR! }
npm ERR! The operation was rejected by your operating system.
The fix most Stack Overflow answers suggest โ slapping sudo in front โ technically works but creates a worse mess down the road. Global packages installed as root can conflict with your user environment and cause even more permission headaches later.
There are three proper fixes. Pick the one that matches your setup.
Why this happens
When Node.js is installed via the official .pkg installer or via some Homebrew setups, it places global modules in /usr/local/lib/node_modules. On macOS (especially post-Catalina with SIP enabled), your user account doesn't have write access to that directory by default. So the moment npm tries to write there, the OS blocks it.
Check who owns the directory:
ls -la /usr/local/lib/ | grep node_modules
If you see root as the owner, that's your problem.
Fix 1: Switch to nvm (the permanent fix)
This is the right long-term approach. nvm (Node Version Manager) installs Node entirely inside your home directory, so npm never needs root access for anything.
# Install nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# Reload your shell config
source ~/.zshrc # or ~/.bashrc if you use bash
# Install the Node version you need
nvm install --lts
nvm use --lts
# Verify it's using the nvm-managed node
which node
# Should output something like: /Users/yourname/.nvm/versions/node/v20.x.x/bin/node
After switching, global installs work without any permission issues:
npm install -g typescript
npm install -g pnpm
One thing to watch: if you had Node previously installed system-wide, make sure your PATH picks up the nvm version first. Run which node and node --version to confirm.
Fix 2: Change npm's global prefix to a user directory
If you need to keep your current Node installation (or you're on a team machine where you can't change the Node setup), redirect npm's global package directory to somewhere your user owns.
# Create a directory for global npm packages
mkdir -p ~/.npm-global
# Tell npm to use it
npm config set prefix '~/.npm-global'
# Add it to your PATH โ add this line to ~/.zshrc or ~/.bash_profile
export PATH=~/.npm-global/bin:$PATH
# Reload
source ~/.zshrc
Verify the config took effect:
npm config get prefix
# Should output: /Users/yourname/.npm-global
Now test a global install:
npm install -g nodemon
nodemon --version
Fix 3: Fix ownership on the existing directory
Use this only if you're the sole user on the machine and don't want to change how Node or npm is configured. You're changing the owner of /usr/local/lib/node_modules to yourself.
# Check your username
whoami
# Change ownership (replace 'yourname' with your actual username)
sudo chown -R yourname /usr/local/lib/node_modules
sudo chown -R yourname /usr/local/bin
Be careful with this one on shared machines โ you're making system directories writable by a user account. On a personal dev Mac it's fine, but understand the trade-off.
Verification
After applying any of the fixes, confirm everything is working:
# Should complete without EACCES error
npm install -g eslint
# Should output a version number
eslint --version
# Check where globals are installed now
npm root -g
npm bin -g
If npm bin -g points to a path that's in your $PATH, you're set.
Tip: Understanding the permissions involved
If you're troubleshooting this across multiple directories or trying to understand what permissions to set, the Unix Permissions Calculator on ToolCraft is handy for working out chmod values visually. I use it when I need to quickly figure out the right permission bits without mentally converting octal.
Lessons from the field
- Never use
sudo npm install -gas a routine fix. It works once but creates owned-by-root files that break subsequent installs without sudo, creating a cycle. - nvm is the default choice for personal machines. It also lets you switch Node versions per project using
.nvmrcfiles, which is worth it on its own. - The prefix approach (Fix 2) is great when working on company machines where you don't control Node installation.
- If you set up a new Mac and clone an existing dotfiles repo, make sure your
.zshrcPATH configuration gets applied before the system PATH โ otherwise nvm installs get shadowed by system Node.

