Cause #1: Missing execute permission on the file
What's actually happening here is the kernel checks the file's mode bits when you try to exec it. If the execute bit (owner, group, or other) isn't set, you get bash: ./script.sh: Permission denied. This is the most common cause, especially after you download a script or copy it from a Windows machine (where there's no concept of an execute bit).
ls -l script.sh
# -rw-r--r-- 1 user user 1234 Jan 1 10:00 script.sh <- no x bits
The fix is to add the execute bit. I usually go with chmod 755 for scripts I want others to read and run, or chmod +x if you only care about your own user. The reason +x works is it adds execute for owner, group, and other based on your umask, but 755 is explicit and predictable.
chmod +x script.sh
# or
chmod 755 script.sh
./script.sh
Real-world trigger: You just pulled a repo from GitHub, and bin/deploy.sh won't run. Git does store the execute bit, but if the file was added on Windows or the repo doesn't have it set, you'll hit this.
If you're on a filesystem that doesn't support Unix permissions (like a FAT32 USB drive), chmod won't stick. In that case, copy the script to your home directory (ext4 or similar) and run it there. That's not a workaround—it's the actual fix, because FAT32 has no concept of mode bits.
Cause #2: Filesystem mounted with noexec
This one sneaks up on you. The file has the execute bit, but the mount options block execution. When a filesystem is mounted with noexec, the kernel refuses to run any binary or script from that mount, regardless of permissions. You'll see Permission denied even as root, which is a clue.
Common culprits: /tmp, /home on some hardened setups, or mounted external drives. Check with mount or findmnt:
findmnt /tmp
# TARGET SOURCE FSTYPE OPTIONS
# /tmp /dev/sda1 ext4 rw,noexec,relatime
If you see noexec in OPTIONS, that's your problem. You have two paths: remount without noexec, or move the script to a location that allows execution. For a quick test, remount:
sudo mount -o remount,exec /tmp
The reason this works is remount changes the mount flags on the fly. But be careful—if noexec was set by a security policy (like on a corporate laptop or a server with SELinux/AppArmor), you might not want to disable it. In that case, put your scripts in /usr/local/bin or your home directory, which is usually mounted with exec.
I've seen this bite people who mount their entire home directory with nodev,noexec for security and then wonder why ./miniconda.sh fails. The installer is trying to execute a shell script from $HOME. Move it to /tmp (if that's exec) or remount.
Cause #3: Root squash on NFS or ownership mismatch
Less common if you're a solo user, but on NFS mounts this is a classic. When an NFS server exports a share with root_squash, the root user on the client is mapped to the anonymous user (nobody). So even if you have the execute bit and the file is owned by root, your client-side root can't run it because the server sees you as 'nobody' with no permissions.
That's the theory. The symptom: sudo ./app fails with Permission denied, but ls -l shows -rwxr-xr-x and you're root. You check ownership and it says root root. But on the server, the file might be owned by a different UID that doesn't map.
First, verify it's NFS:
mount | grep nfs
# server:/export on /mnt/data type nfs4 (rw,relatime,vers=4.2)
Then check the actual ownership from the client's perspective. If UIDs don't match (client UID 1000 vs server UID 1001), you'll get weird permission errors. The fix is either:
- Align UIDs across systems (use the same UID for the same user).
- Ask the server admin to add
no_root_squashfor your client IP—but that's a security risk, so only if you control the server. - Use
sudo -u nobody? No, that's wrong. The real fix is to fix the mapping.
I ran into this on a cluster where compute nodes mounted /home via NFS. Users could compile but not run their own binaries because the automounter used squash. The fix was to ensure all nodes had the same UID/GID database (via LDAP or manual passwd entries).
If you're not on NFS, ownership mismatch can still happen if a file is owned by another user and you lack read/execute. But if you're root, that shouldn't stop you—except, as above, when root_squash is in play.
Quick-reference summary
| Cause | Symptom | Check | Fix |
|---|---|---|---|
| No execute bit | Permission denied for your user, works with bash script.sh | ls -l shows no x | chmod +x file |
| noexec mount | Fails even as root, file has x bit | findmnt /path shows noexec | mount -o remount,exec /path or move file |
| NFS root_squash / UID mismatch | Fails as root on NFS, file has x bit | mount | grep nfs and id on server | Align UIDs or adjust export options |
That's the order I'd run through. Start with chmod, because nine times out of ten that's it. Move to mount options if you're on /tmp or an external drive. Only start blaming NFS when you're sure the first two are clean.