“bash: ./script.sh: Permission denied”: why your script won’t run and how to fix it
To run a file as <code>./script.sh</code>, the system needs two things: the file must be marked executable, and the disk it lives on must allow running programs. Miss either and the shell says <i>Permission denied</i>, even if you own the file and can read it.
The messages you might see
| Where | Wording |
|---|---|
| bash | bash: ./script.sh: Permission denied |
| zsh (macOS default) | zsh: permission denied: ./script.sh |
| sh / dash | sh: 1: ./script.sh: Permission denied |
| CI logs, Docker, make | exit code 126 or /bin/sh: ./entrypoint.sh: Permission denied |
Exit code 126 means “found it, but couldn’t run it”. Compare 127, which means “no such command”.
Step 1: look at the permissions
ls -l script.sh
-rw-r--r-- 1 sam sam 210 Nov 4 10:00 script.sh # no x anywhere: not executable
chmod +x script.sh
ls -l script.sh
-rwxr-xr-x 1 sam sam 210 Nov 4 10:00 script.sh # now it runs
The x letters are the execute bits for owner, group and everyone else. In numbers, 644 becomes 755. Our chmod 755 guide explains the digits, and the number base converter shows the octal-to-binary idea behind them.
Causes and fixes
| Situation | Fix |
|---|---|
| A new file you just wrote or downloaded | chmod +x script.sh. Downloads and new files are never executable by default |
It’s on /tmp, a USB stick, a network share or /dev/shm mounted noexec | Check with findmnt -T script.sh (look for noexec). Run bash script.sh, or move it to your home folder |
| Cloned from git and the bit is missing | Git stores one execute bit per file. Set it in the repo: git update-index --chmod=+x script.sh, then commit. Committing from Windows usually loses it, so this is the fix there too |
Docker: exec ./entrypoint.sh: permission denied | COPY --chmod=755 entrypoint.sh /app/ (BuildKit), or RUN chmod +x /app/entrypoint.sh after the copy |
| CI runner (GitHub Actions, GitLab and others) | Fix the bit in git as above, or call it as bash ./script.sh in the step |
Typed script.sh without ./ | That gives command not found, not Permission denied: the current folder isn’t on PATH. Use ./script.sh |
sudo ./script.sh still denied | Root can’t run a file with no execute bit either, and noexec applies to root. chmod +x first |
| The folder above isn’t enterable | Every parent folder needs x for you to reach the file: namei -l /path/to/script.sh shows each step |
Running it without the execute bit
bash script.sh asks bash to read the file, so only read permission is needed and noexec doesn’t apply. It ignores the #! line, though, so use the right interpreter: python3 tool.py, sh script.sh, and so on.
A close cousin: bad interpreter ^M
/bin/bash^M: bad interpreter: No such file or directory means the permissions are fine but the file has Windows line endings, so the first line asks for a program called bash\r. Convert it with sed -i 's/\r$//' script.sh or dos2unix, and see LF will be replaced by CRLF to stop git doing it again. Compare the two versions in the text diff tool if you want to see what changed.
Related
SSH refusing your key because its permissions are too open is the opposite problem: see SSH key permissions are too open. Docker itself saying permission denied? See docker.sock permission denied. sudo itself refusing you? See not in the sudoers file. npm install -g hitting EACCES on /usr/local? See npm EACCES global install.
Questions
Why do I get Permission denied when running ./script.sh?
The file isn't marked executable, or it sits on a drive mounted noexec. Run chmod +x script.sh, or run it with bash script.sh.
What does exit code 126 mean?
The shell found the file but couldn't execute it, usually because it lacks the execute bit or is on a noexec mount. Exit code 127 means the command wasn't found at all.
How do I keep the execute bit in git?
Run git update-index --chmod=+x script.sh and commit. Git stores the executable flag per file, so everyone who clones gets it.
Why does sudo not fix Permission denied for a script?
Even root can't execute a file with no execute bit, and noexec mounts block root too. Add the bit with chmod +x or run it through bash.
Published .