"Permission denied" when running a script: 5 causes and fixes
Fix "bash: ./script.sh: Permission denied" and "bad interpreter" on Linux: execute bits, noexec mounts, directory permissions, Windows line endings and shebangs.
by OhSudo ·
You wrote (or downloaded) a script, you run it, and Linux says no:
-bash: ./backup.sh: Permission deniedsudo: unable to execute ./backup.sh: Permission denied-bash: ./deploy.sh: /bin/bash^M: bad interpreter: No such file or directoryRunning a file as a program needs more than the file existing: it needs the execute permission, a filesystem that allows executing, access to every directory on the way, and a valid interpreter line. Here’s how to check each one, fastest first.
TL;DR
ls -l script.sh # is there an x in the permissions?chmod u+x script.sh # add it for the ownerfindmnt -no OPTIONS --target script.sh # "noexec" here blocks every script on that filesystembash script.sh # run it through the interpreter insteadcat -A script.sh | head -3 # ^M at line ends = Windows line endingssed -i 's/\r$//' script.sh # convert them to Unix line endings1. The file isn’t executable
The most common cause by far. Look at the permissions:
ls -l scripts/backup.sh-rw-r--r-- 1 guest guest 132 Nov 1 10:00 scripts/backup.shThere’s no x, so nobody may execute it — not even the owner. Add the execute permission for yourself:
chmod u+x scripts/backup.sh./scripts/backup.shchmod +x would add it for everyone (owner, group and others). Prefer u+x unless other users really need to run the script. Not sure what a mode like 750 means? The chmod Calculator shows it.
Tip
Scripts in a Git repository lose the execute bit when the repository is used from Windows, or when the bit was never committed. Store it in Git once with git update-index --chmod=+x scripts/backup.sh and commit — everyone who clones gets an executable script.
2. The filesystem is mounted with noexec
The file is executable, you’re the owner, and it still says “Permission denied”? Check where it lives. Directories like /tmp, /dev/shm, USB drives and, on hardened servers, even /home are often mounted with the noexec option, which forbids running any program from them:
findmnt -no OPTIONS --target /tmprw,nosuid,nodev,noexec,relatimenoexec is a security measure (attackers like to drop programs in /tmp), so don’t remove it. Instead:
-
run the script through its interpreter — this only needs read permission:
Terminal window bash /tmp/install.sh -
or move the script somewhere that allows executing, such as your home directory or
/usr/local/bin.
3. A directory on the path blocks you
To run /opt/tools/bin/check.sh, you need the execute (x, “search”) permission on every directory of the path: /opt, /opt/tools and /opt/tools/bin. One locked directory is enough to get “Permission denied”, even if the script itself is perfect. namei shows the permissions of each step at once:
namei -l /opt/tools/bin/check.shFix the directory that’s missing the x for you (for example sudo chmod o+x /opt/tools), or ask its owner to.
4. “bad interpreter: No such file or directory” — Windows line endings
This one confuses everybody, because the file obviously exists:
-bash: ./deploy.sh: /bin/bash^M: bad interpreter: No such file or directoryThe script was saved with Windows line endings (CRLF). Every line ends with an invisible carriage return, ^M — so Linux looks for an interpreter called /bin/bash␍, which doesn’t exist. Make the invisible characters visible:
cat -A scripts/deploy.sh | head -3#!/bin/bash^M$echo "Deploying webapp..."^M$echo "Deployed."^M$Remove the carriage returns:
sed -i 's/\r$//' scripts/deploy.sh./scripts/deploy.sh(dos2unix scripts/deploy.sh does the same, if it’s installed.) If the shebang is #!/usr/bin/env bash, the same problem shows up as /usr/bin/env: 'bash\r': No such file or directory.
Tip
Stop it from coming back: in Git, add a .gitattributes file with the line *.sh text eol=lf, and set your editor to use LF line endings for shell scripts.
5. The shebang points to an interpreter that isn’t there
The same “bad interpreter” message, without the ^M, means the first line names a program that doesn’t exist on this machine — for example #!/usr/local/bin/bash on a server where Bash lives in /usr/bin/bash, or a Python version that isn’t installed:
head -1 script.sh # what does the script ask for?command -v bash # where is bash on this machine?#!/usr/bin/env bash finds bash wherever it is in the PATH, which makes scripts portable between distributions.
Note
RHEL / Rocky / AlmaLinux: if a script runs fine from your shell but fails when a service runs it (systemd, a web server, cron jobs run by a confined service), SELinux may be blocking it. Check the audit log with sudo ausearch -m avc -ts recent, look at file labels with ls -Z, and restore the default labels with sudo restorecon -Rv /path/to/scripts. On Ubuntu and Debian, AppArmor plays a similar role for some services.
Quick checklist
| Symptom | Cause | Fix |
|---|---|---|
Permission denied, no x in ls -l |
Not executable | chmod u+x script.sh |
Permission denied, x is there |
noexec mount |
bash script.sh, or move it |
Permission denied for a script in another user’s tree |
Directory without x |
namei -l path, fix that directory |
bad interpreter with ^M |
Windows line endings | sed -i 's/\r$//' script.sh |
bad interpreter without ^M |
Wrong shebang | #!/usr/bin/env bash |
$ ./practice.sh
Practice this in the terminal
Open the Terminal Playground with this article’s scenario loaded. It is safe and runs in your browser.
Found a mistake? Let us know at contact@ohsudo.com.