Permission Denied Even After chmod? Here's Why
You ran chmod and still get 'permission denied.' The file's mode is only the first gate. A field guide to the layers underneath: parent directories, ownership, mounts, SELinux, and ACLs.
You checked the permissions. You even ran chmod 777 in frustration. And the shell still says:
-bash: ./deploy.sh: Permission denied
When a correct-looking mode still gets rejected, the instinct is to widen the mode further — but that’s treating the wrong patient. A file’s permission bits are only the first of several checks the kernel makes. When access is denied and the mode looks fine, the block is almost always one of the layers around the file: the directories above it, who owns it, the filesystem it lives on, or a security module sitting over the top. This is the field guide to those layers.
If you need the mode itself decoded first — what 755 or 644 actually grants — start with what chmod 755 means. Here we assume the mode reads correctly and ask why it isn’t enough.
First, split the problem in two
Before chasing exotic causes, settle one question: is it the file’s own mode, or something else? Answer it without changing anything — one command surveys the whole path at once:
namei -l /home/alice/app/deploy.sh
namei -l walks every component of the path from / down and prints the mode and owner of each one. In a few lines it exposes the two most common culprits — a parent directory you can’t traverse, and a file you don’t own — no guessing, and nothing widened. Pair it with id (who am I, and which groups am I in?) and a plain ls -l on the file, and you can usually name the gate before touching a single permission. (namei is part of util-linux; on macOS, walk the path by hand with ls -ld on each directory.)
Resist the reflex to chmod 777 “just to test.” It changes state you then have to remember to undo; chmod u+rwx only touches the owner’s bits, which is useless when the problem is that you’re not the owner; and testing with sudo answers a different question — whether root can get in, not whether you can.
Cause 1: the path, not the file (a missing x upstream)
This is the single most common “denied despite a good mode.” To open /home/alice/app/deploy.sh, you need execute (traverse) permission on every directory along the way: /, /home, /home/alice, /home/alice/app. Miss it on any one of them and access to everything beneath is denied — the file’s own mode never even gets consulted.
ls -ld / /home /home/alice /home/alice/app
Look for any line without an x in the right place. The fix is to restore traverse on the offending directory:
chmod o+x /home/alice # let others traverse (not read/list)
Note that o+x grants traverse only, not the ability to list the directory’s contents — often exactly what you want for a parent directory that shouldn’t be browsable. This is why chmod 777 on the file did nothing: the problem was a directory three levels up. (The pillar explains why directory execute works this way.)
There’s a second half to this that the “why is it denied” search often needs: which permission governs which operation. Reading or running an existing file is about the file’s own bits (plus x on every parent). But creating, deleting, or renaming a file is governed entirely by the parent directory — you need w and x on the directory, and it makes no difference whether the target file itself is writable. A read-only file in a directory you can write is deletable; a writable file in a directory you can’t write is not.
| To… | You need… |
|---|---|
| Open, read, or execute an existing file | the file’s own permission, plus x on every parent directory |
| Create, delete, or rename an entry | w and x on the parent directory (the file’s own bits don’t matter) |
| List and work with a directory’s entries | r and x on the directory |
So a “permission denied” from rm, mv, or creating a new file almost always points at the directory’s mode, not the file’s (the sticky bit adds one more condition on deletes — Cause 6).
Cause 2: ownership, not permission
Permissions are granted per class — owner, group, other. Which class applies to you depends on who you are: are you the file’s owner, a member of its group, or neither? Check the file’s owner and group against your own identity:
ls -l deploy.sh # -rwxr-x--- 1 root staff ...
id # uid=1000(alice) gid=1000(alice) groups=1000(alice)
Here the file is owned by root:staff with mode 750, and you’re alice, not in staff. You fall through to other, which 750 grants nothing — denied, no matter how the owner’s bits look. chmod won’t help you here, because you’re editing what each class may do, not which class you’re in. The real fixes are ownership tools:
id -nG alice # first: which groups does alice actually belong to?
sudo chown alice deploy.sh # make alice the owner, or…
sudo chgrp developers deploy.sh # …move the file into a group she's already in, or…
sudo usermod -aG staff alice # …add alice to the file's group, then re-login
The middle command only helps if alice is really in developers — chgrp-ing the file to staff would change nothing, because she still isn’t a member. That’s the trap: moving a file into a group the user doesn’t belong to grants them nothing. This is the honest answer to most chmod 777 reflexes: the person couldn’t get in because they were in the wrong class, and the right fix is chown/chgrp plus a sane mode — not throwing the file open to the entire world.
Cause 3: the filesystem says no (noexec, read-only)
A mount option can override the mode entirely. Two bite often:
noexec— blocks direct execution of a file on this filesystem,xbit or not. Common on/tmp,/dev/shm,/run, USB drives, and hardened containers. Running./scriptthere yields “Permission denied” no matter the mode. (Feeding the file to an interpreter —bash ./script,python ./script— sidestepsnoexec, because that reads the file rather than executing it directly; which is exactly why hardening it this way isn’t airtight.)ro(read-only) — you cannot write,wbit or not. Common on container image layers, mounted ISOs, and snapshots.
Check what filesystem a path lives on and how it’s mounted:
findmnt -T /tmp/deploy.sh # shows the mount and its options
# → TARGET SOURCE FSTYPE OPTIONS
# /tmp tmpfs tmpfs rw,nosuid,nodev,noexec
There it is: noexec. The fix isn’t chmod — it’s to move the file to a filesystem that allows execution (say, under your home directory), or, if you control the mount, remount without the option. On macOS, use mount with no arguments to list mounts and their flags.
Cause 4: a security module (SELinux, AppArmor)
On RHEL, Fedora, CentOS, and their kin, SELinux enforces a second permission system on top of the mode, keyed on security contexts rather than user and group. A web server can be denied a file that is world-readable, because the file’s context is wrong. Tell-tale signs: it happens on a fresh RHEL box, only for a service (not for you at the shell), and the mode looks perfect.
getenforce # Enforcing / Permissive / Disabled
ls -Z deploy.sh # shows the SELinux context
sudo ausearch -m avc -ts recent # recent "access vector cache" denials
If ausearch shows an AVC denial for your file, the mode is a red herring. The usual fix is to restore the correct context rather than to touch the mode:
sudo restorecon -v /var/www/html/deploy.sh # reset to the policy default
On Ubuntu and SUSE the equivalent is AppArmor: check sudo aa-status and look in dmesg for apparmor="DENIED" lines; the profile lives in /etc/apparmor.d/. In both cases you’re debugging policy, not file bits.
Cause 5: an ACL overriding the mode
chmod sets the three classic classes, but a file can also carry a POSIX ACL — extra per-user or per-group rules that sit alongside the mode. A + at the end of the mode in ls -l is the giveaway:
ls -l report.csv # -rw-r-----+ ← the trailing + means an ACL is present
getfacl report.csv # shows the real rules
The point is that an ACL makes the simple owner/group/other view incomplete — the three mode bits are no longer the whole story. Extra ACL entries can grant access beyond the simple mode view, while the ACL mask can limit the effective permissions of named users, named groups, and the owning group. So getfacl may list group::rwx but mask::r--, making the real grant only r--. Note too that in ls -l, the group triad shows the mask, not the owning group’s own entry — another reason the plain listing can mislead. Read getfacl and its “effective” column before changing anything; then adjust the specific entry you need (for example setfacl -m u:alice:rx report.csv). Do not use setfacl -b as a default fix: it removes extended rules that may be intentional.
Cause 6: special bits and filesystem flags
Sometimes the mode is being obeyed — you’ve just misread what it does (the chmod calculator decodes any four-digit mode, special bits included):
- The sticky bit (
1777, as on/tmp) means an entry in a shared directory can be deleted or renamed only by that entry’s owner, the directory’s owner, or a privileged user. “Operation not permitted” when removing someone else’s temp file is the sticky bit doing its job, not a bug. - On Linux, a setuid bit on an interpreted script is ignored.
4755 install.shwill not run as its owner the way a setuid binary can. Treat setuid scripts as a design error, not a permission fix. - The immutable attribute (Linux, on filesystems that support it — ext4, XFS, Btrfs, and similar) blocks writes even for root, and it’s invisible to
chmod. If bothchmodandchownseem to have no effect, check it withlsattr/chattr:
lsattr deploy.sh # an 'i' means immutable
sudo chattr -i deploy.sh
macOS has no chattr; its closest equivalents are the uchg/schg flags via chflags and, on system paths, System Integrity Protection — both of which can also deny root.
When it isn’t “permission denied” at all
A couple of failures wear the same costume and send you down the wrong path:
bad interpreter: No such file or directoryon a script isn’t a permission problem — it’s a broken shebang (wrong interpreter path, or WindowsCRLFline endings on the#!line).- A full disk or exhausted inodes reports
No space left on device, not “permission denied.” If you see that wording, stop looking at modes and checkdfanddf -i. Permission deniedwhile writing to an NFS mount can beroot_squash, which maps the remote root tonobody— so evensudois refused on the export. That one genuinely is a permission problem, just not one the file’s bits can fix.
If you’re chasing a scheduled job that runs by hand but not from cron, that’s a related-but-different environment puzzle — why didn’t my cron job run is the field guide for that half.
A diagnostic checklist
When chmod doesn’t clear a “permission denied”:
- Survey without changing anything.
namei -l /full/path,id, andls -lon the file usually name the gate before you touch a permission — don’tchmod 777to test. - Walk the path. A missing
xon any ancestor blocks everything beneath it. And remember create/delete/rename needswandxon the parent, not on the file. - Check ownership against
id. If you’re not the owner and not in the group, you’re “other” — fix withchown/chgrp(into a group you’re actually in), not a wider mode. - Check the mount.
findmnt -T <path>:noexecblocks execution,roblocks writes, regardless of bits. - Check for a security module.
getenforce/ls -Zfor SELinux,aa-status/dmesgfor AppArmor. - Check for an ACL. A
+inls -l→getfacl; watch themaskline. - Check the exotic bits. Sticky-bit deletes, setuid-on-a-script, and
lsattrfor immutable.
Work down that list and the real gate reveals itself — and it’s rarely the one chmod controls. To confirm what a mode actually grants along the way, the chmod calculator turns any octal or symbolic value into its rwxr-xr-x meaning, with the file-versus-directory distinction spelled out. And once the gate is open, chmod 644 vs 755 covers the mode to leave behind — including how to apply it to a tree without making every file executable.