Bash Error Guide: 'Permission denied' — Fix Exec Bits, Ownership, and Mounts
Fix 'Permission denied' in Bash: set the executable bit, correct ownership and ACLs, handle noexec mounts, missing shebangs, and directory traverse permissions.
- #bash
- #automation
- #troubleshooting
- #errors
Stuck on this Bash & Python Automation error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
Permission denied is the kernel telling you the current user lacks the rights for the operation you attempted — executing a file, reading it, writing to it, or traversing a directory to reach it. In Bash automation it shows up in two distinct shapes. The shell’s own message when it cannot execute a file:
bash: ./deploy.sh: Permission denied
And the EACCES error surfaced by a command that could not open or write a path:
./deploy.sh: line 22: /var/log/deploy.log: Permission denied
They look similar but have different fixes: the first is about the executable bit and how the file is invoked; the second is about read/write access to a target path.
Symptoms
- Running
./script.shfails immediately withPermission denied, before any script output appears. - A script runs, then fails partway when it tries to write a log, PID file, or output directory.
- The same script works as
root/sudobut not as the service user (or vice versa). - A file you can
lsyou cannotcat— or a directory you can see you cannotcdinto. - Code deployed onto a
/tmp, NFS, or removable mount refuses to execute even with the exec bit set.
Common Root Causes
- Missing executable bit — the file was created, copied, or checked out without
+x(common aftergiton some setups,scp, or unzip). - No shebang / wrong interpreter path — invoking
./scriptwith no#!line, or a shebang pointing at an interpreter that isn’t there. - Wrong ownership or mode on the target path — the process user cannot write to a log directory, PID file, or output location owned by another user.
- Missing directory execute (traverse) bit — you need
xon every directory in a path to reach a file inside it; a missingxon a parent blocks access even when the file itself is readable. noexecmount option — filesystems like/tmp,/dev/shm, or NFS mountednoexecrefuse to execute any file regardless of its mode.- ACLs or SELinux/AppArmor — extended ACLs or mandatory access control deny access even when classic
rwxbits look correct. - Running the wrong user under cron/systemd — the job runs as a user without rights to the paths it touches.
- Immutable attribute — a file with the
iattribute (chattr +i) cannot be written even by root until the attribute is cleared.
Diagnostic Workflow
Start with the file’s mode and ownership, and who you actually are:
ls -l ./deploy.sh
id # your uid/gid and groups
namei -l ./deploy.sh # permissions of every component in the path
namei -l is the fastest way to spot a missing traverse (x) bit on a parent directory. For an execute failure, confirm the exec bit and the shebang:
stat -c '%A %U:%G %n' ./deploy.sh # human-readable mode + owner
head -n1 ./deploy.sh # is there a valid #!/usr/bin/env bash ?
If the mode looks correct but execution still fails, suspect the mount:
findmnt -no OPTIONS -T ./deploy.sh # look for 'noexec'
mount | grep "$(df --output=target ./deploy.sh | tail -1)"
Check for ACLs and, on RHEL-family systems, SELinux denials:
getfacl ./deploy.sh
lsattr ./deploy.sh # look for the 'i' immutable flag
sudo ausearch -m avc -ts recent 2>/dev/null | tail # SELinux denials
For a write failure, verify the process user can actually write the target directory:
sudo -u deploy test -w /var/log && echo writable || echo "deploy cannot write /var/log"
Example Root Cause Analysis
A CI job checked out a repo and ran ./scripts/migrate.sh, failing with:
/bin/sh: ./scripts/migrate.sh: Permission denied
ls -l told the story:
$ ls -l scripts/migrate.sh
-rw-r--r-- 1 runner runner 1843 Jul 9 02:11 scripts/migrate.sh
The file was mode 644 — readable but not executable. The repo had been checked out on a filesystem that dropped the exec bit, and nobody had chmod +x’d it. Because the CI step invoked the file directly (./scripts/migrate.sh) rather than through an interpreter, the missing x bit was fatal.
Two fixes, one tactical and one durable:
# Tactical: make it executable
chmod +x scripts/migrate.sh
# Durable: record the exec bit in git so it survives checkout everywhere
git update-index --chmod=+x scripts/migrate.sh
git commit -m "Mark migrate.sh executable"
An equally valid workaround that sidesteps the exec bit entirely is to invoke the interpreter explicitly, which only needs read permission on the file:
bash scripts/migrate.sh # runs even if the exec bit is missing
Prevention Best Practices
- Commit the exec bit — use
git update-index --chmod=+x(orgit addwith the mode already set) so scripts stay executable across every clone and CI runner. - Always include a correct shebang —
#!/usr/bin/env bash— so the right interpreter is used and the file is self-describing. - Run automation as a dedicated user and give that user explicit ownership of the directories it must write (logs, PID files, output), rather than reaching for
chmod 777. - Never use
chmod 777as a fix — it hides the real ownership problem and opens a security hole; grant the minimum needed instead. - Mind
noexecmounts — don’t place executables onnoexecfilesystems like/tmp; if you must, invoke them viabash script.shor move them to an exec-permitted path. - Check ACLs and SELinux when classic bits look right but access is still denied; label files correctly rather than disabling enforcement.
- Preflight write targets — early in the script, verify the log/output directories are writable and fail fast with a clear message.
Quick Command Reference
ls -l FILE # mode and owner
namei -l PATH # permissions of every path component
stat -c '%A %U:%G %n' FILE # readable mode + ownership
chmod +x FILE # add the executable bit
git update-index --chmod=+x FILE # persist exec bit in git
findmnt -no OPTIONS -T FILE # detect a noexec mount
getfacl FILE # inspect ACLs
lsattr FILE # detect the immutable (i) attribute
sudo -u USER test -w DIR # can this user write here?
Conclusion
Permission denied (EACCES) comes down to one question: does the running user have the required right — execute, read, write, or directory traverse — on the specific path involved? Separate the two cases: a failure to run a script is usually a missing exec bit, a bad shebang, or a noexec mount; a failure inside a running script is usually ownership or write permission on a target directory. Diagnose with ls -l, namei -l, and findmnt, fix with the least privilege that works (commit the exec bit, grant the right user ownership), and never paper over it with chmod 777.
Fixed it? Get 500 Bash & Python Automation & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.