← All Articles

Permission Denied (EACCES) on Linux and macOS — Fix It Properly, Not With sudo

Read the Full Sentence

"Permission denied" is the first half of a sentence the OS is telling you. The second half is always one of: you're not the owner, the execute bit is missing, or a parent directory blocks the path. The fix depends on which one it is — and reaching for sudo blurs all three, which is exactly why sudo-fixes multiply.

Step 1: Identify the Real Failure

ls -la ./script.sh
# -rw-r--r-- 1 root root ...  ← owned by root, you can't write
# -rw-r--r-- 1 you  you  ...  ← you own it, but no exec bit

Three symptoms, three meanings:

  • -rw-r--r-- and you run ./script.sh → "Permission denied" because the execute bit is missing.
  • Owned by root, you're editing → write denied regardless of read permissions.
  • cd /dir fails with permission denied → a parent directory lacks the execute (search) bit for you, even if the target is world-readable.

Step 2: The Three Correct Fixes

A. Missing execute bit (scripts, binaries)

chmod +x ./script.sh

If you downloaded it and it still refuses despite +x: check the filesystem — files on a mounted volume with noexec can never run. mount | grep noexec confirms it; remount or move the file.

B. Wrong ownership (files created by sudo or root)

# who owns it, and who should:  
ls -la ./file
sudo chown $(whoami):$(id -gn) ./file

The classic origin story: you once ran one command with sudo, it created root-owned files, and now every normal command fails. Fix ownership of those files — do not elevate the next fifty commands.

C. Directory search permission

namei -l /path/to/the/file

namei -l prints permissions for every component in the path. When a deep file is unreachable, this shows you exactly which directory in the chain is missing the x bit for your user or group.

The Case Study Everyone Hits: npm EACCES

"npm ERR! EACCES: permission denied, access '/usr/local/lib/node_modules'" happens when npm's global prefix points into a root-owned directory — usually because at some point in history, someone ran sudo npm install -g (possibly following a tutorial).

Three valid fixes, best to worst:

  1. Use a version manager (nvm/fnm/volta). Node and its global packages live in your home directory; the problem cannot exist. This is the real answer.
  2. Re-point the npm prefix: npm config set prefix ~/.npm-global, then add ~/.npm-global/bin to PATH.
  3. Fix ownership once: sudo chown -R $(whoami) /usr/local/lib/node_modules /usr/local/bin — the legacy unblock.

What NOT to do: sudo npm install -g again. It will "work," and it will leave more root-owned debris for future-you.

SSH and Key Permissions

A separate and famous variant: Permissions 0644 for 'id_rsa' are too open. SSH refuses keys readable by group/others. Fix:

chmod 600 ~/.ssh/id_*
chmod 700 ~/.ssh
chmod 644 ~/.ssh/*.pub   # public keys are fine open

When sudo Is Actually the Answer

Legitimate sudo uses: system-wide package manager installs, edits to files under /etc you genuinely administer, service management. Illegitimate: making your app's own files writable, running your build, "it works with sudo" on a personal project. The test: if the command operates on system resources, sudo. If it operates on your project, the permissions are wrong — fix them.

Quick Decision Tree

  1. Run ls -la and namei -l.
  2. No x bit? → chmod +x.
  3. Root-owned but shouldn't be? → chown.
  4. Parent dir blocks? → fix the specific dir's mode.
  5. Still stuck? Check noexec mounts, ACLs (getfacl), and immutable flag (lsattr showing an i).
  6. Only then reconsider whether the task really needs elevation — it usually doesn't.