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 bitThree 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 /dirfails 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.shIf 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) ./fileThe 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/filenamei -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:
- 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.
- Re-point the npm prefix:
npm config set prefix ~/.npm-global, then add ~/.npm-global/bin to PATH. - 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 openWhen 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
- Run
ls -laandnamei -l. - No x bit? → chmod +x.
- Root-owned but shouldn't be? → chown.
- Parent dir blocks? → fix the specific dir's mode.
- Still stuck? Check noexec mounts, ACLs (
getfacl), and immutable flag (lsattrshowing ani). - Only then reconsider whether the task really needs elevation — it usually doesn't.