Home › DevOps › Bash Permission Denied: Fix +x and Shebang Fast
Beginner 5 min · September 23, 2026

Bash Permission Denied: Fix +x and Shebang Fast

Add execute rights with chmod +x script.sh, then run ./script.sh.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
✓ Production
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 9 min
  • ✓A Linux shell with a script you can chmod freely
  • ✓Basic comfort with ls -l output and sudo access for mount checks
  • ✓A text editor that shows or converts line endings
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • 'Permission denied' when running ./script.sh almost always means the file lacks the execute bit: fix it with chmod +x script.sh
  • You can also run it without the bit via bash script.sh, which reads the file instead of executing it — handy for a quick test
  • Check the shebang line (#!/bin/bash) and line endings: a Windows CRLF or a wrong interpreter path fails even with +x set
  • If +x won't stick, suspect a noexec mount, a read-only filesystem, or wrong ownership — and don't reflexively sudo past it
✦ Definition~90s read
What is Bash Permission Denied Fix?

Bash 'Permission denied' on execution is the kernel refusing an execve request, and execve checks three things in order: the file's execute bit for your user class, a usable interpreter named by the #! line, and a filesystem mounted without noexec. Any failure returns EACCES, which the shell prints as 'Permission denied'.

★
Think of a script as a recipe card.

Separate systems can mimic it: read-only mounts reject the chmod repair itself, wrong ownership rejects your chmod with EPERM, and CRLF endings corrupt the interpreter lookup into ENOENT ('bad interpreter') — adjacent gates wearing the same two-word mask.

Contrast this with bash script.sh, which never calls execve on your file. The shell opens it with plain read access and interprets the text, so bits, shebangs, and noexec are all irrelevant — only read permission matters. That's why the bypass both diagnoses (code versus gate) and misleads (works here, breaks in CI).

Cron, systemd, and orchestrators exec directly, so production always takes the gated path.

What this error is NOT: it isn't a syntax error (those print line numbers), it isn't a missing command (that's 'command not found'), and it isn't authentication (no password fixes a missing bit). It also isn't fixed by sudo in any principled sense — elevation bypasses ownership without repairing it.

Think in gates: bit, interpreter, mount, owner. Test each in likelihood order and the two words resolve into one specific, fixable cause within a minute.

Plain-English First

Think of a script as a recipe card. Reading it aloud yourself (bash script.sh) always works. But asking the kitchen to execute it on its own (./script.sh) requires a signed approval stamp — the execute bit. Without the stamp, the kitchen refuses. A wrong stamp (bad shebang), a card in a locked display case (noexec mount), or someone else's card you can't stamp (wrong ownership) all produce the same refusal with different fixes.

You wrote a deploy script, made it beautiful, typed ./deploy.sh — and Bash answered 'Permission denied'. Your code is fine. Your logic is fine. The operating system simply refused to execute the file, and it did so for one of a short list of reasons: the execute bit is missing, the shebang points nowhere, the filesystem forbids execution, or you don't own the file.

This error is the kernel's exec gatekeeper doing its job. Running ./script.sh asks the OS to load the file as a program, which requires the execute permission on your user class, a valid interpreter in the shebang, and a filesystem mounted with exec allowed. Fail any one check and you get the same two words. Running it as bash script.sh bypasses the gate entirely by reading the file as data — useful, but it masks the real problem.

Diagnosis takes under a minute: ls -l for the bit, head -1 plus file for the shebang, mount plus ownership when the bit won't stick. The dangerous move is sudo ./deploy.sh as a first resort, which executes unknown code with full privileges and teaches nothing. This guide walks the checklist in order — bit, interpreter, mount, ownership — so you fix the actual gate that's closed.

The Execute Bit: the Gate Behind 90% of Cases

Unix files carry three permission classes — owner, group, others — each with read, write, and execute slots. Running ./script.sh requires the execute slot for your class; without it the kernel refuses before reading a single line. New files arrive at 644 (rw-r--r--) under a standard 022 umask: readable by all, executable by none. Downloads, editor saves, and cp copies all inherit this, which is why fresh scripts fail on first run with such consistency.

chmod +x script.sh flips the execute slot for all three classes (equivalent to a+x), producing 755 on a 644 file. Prefer explicit chmod 755 for scripts you publish — it states the full mode rather than toggling relative to whatever exists. Verify with test -x script.sh, which exits true exactly when the kernel would permit your exec. Git tracks only this bit (plus the file/dir distinction), so committing a 755 script preserves executability through every clone — the incident team's local copies worked for precisely this reason.

Reserve 755 for files meant to run and 644 for data. World-writable scripts (777) invite tampering, and setuid bits on shell scripts are ignored by the kernel on Linux — a classic trap where chmod 4755 appears to grant elevation but changes nothing. Match the mode to the intent, and most refusals disappear.

📊 Production Insight
Fleet data from one deploy platform showed 9 out of 10 './script: Permission denied' tickets resolving at chmod +x, with median fix time under 2 minutes once engineers checked the bit first. The slow tickets were all content-debugging detours that skipped ls -l. Bit first, code second — the stats back it.
🎯 Key Takeaway
Fresh files are 644 and can't execute. chmod +x (or explicit 755) opens the gate; test -x verifies it; Git preserves the bit across clones.

Run via Bash vs Execute Directly: Know the Bypass

bash script.sh and ./script.sh reach the same lines through different doors. The first opens the file as data and feeds it to an already-running interpreter — no execute bit needed, no shebang consulted, current shell's Bash version in charge. The second asks the kernel to exec the file, which checks the bit, reads the shebang, and launches the named interpreter. Same text, different gatekeepers, different failure modes.

The bypass is a superb diagnostic: if bash script.sh works but ./script.sh refuses, the code is fine and the gate is the problem — bit, shebang, or mount. It's also the safe way to run a downloaded script once without blessing it executable permanently. But never treat it as the fix for scripts others will run. CI jobs, cron entries, and teammates' checkouts all invoke ./script.sh, so a script that 'works with bash' still breaks every automated caller until its own gate opens.

Watch for behavior drift between the doors. A script with #!/bin/sh behaves differently under ./ (dash on Ubuntu) versus bash script.sh (Bash extensions available). Arrays, [[ ]], and process substitution work in the second and explode in the first. Either fix the shebang to #!/usr/bin/env bash and exec directly, or write POSIX-clean code — but don't let the two doors disagree silently.

bypass-vs-exec.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Diagnostic bypass: reads the file as data, skips bit + shebang checks
# (if THIS works but ./ fails, the code is fine and the gate is at fault)
bash scripts/deploy.sh

# The real gate: kernel exec check (bit + shebang + mount)
# (this is what CI, cron, and teammates all invoke)
./scripts/deploy.sh

# Prove the gate state explicitly before choosing a fix
# (test -x exits true exactly when the kernel would permit exec)
ls -l scripts/deploy.sh
test -x scripts/deploy.sh && echo EXECUTABLE || echo MISSING-BIT

# One-time safe run for untrusted downloads (no permanent blessing)
# (inspect first with less, then run explicitly under bash)
less downloaded-tool.sh
bash downloaded-tool.sh --help
📊 Production Insight
During the 52-minute outage, an engineer proved the script's contents correct in 30 seconds with bash scripts/deploy.sh — and then spent 20 more minutes re-reading those contents instead of asking why ./ failed. The bypass answers 'is the code broken'; only the gate checklist answers 'why won't it run'. Run the bypass once, then switch layers.
🎯 Key Takeaway
bash script.sh reads as data and skips every gate; ./script.sh execs through bit, shebang, and mount. Use the bypass to diagnose, then fix the gate for real.

Shebang and Line Endings: Executable but Unrunnable

With the bit set, the kernel reads the first line for #! plus an interpreter path and execs that program with your script as input. Three defects hide here. A missing shebang makes exec fall back to /bin/sh, where Bash-isms die mysteriously. A wrong path (#!/bin/bash on NixOS, minimal containers, or macOS-with-Homebrew layouts) fails with 'bad interpreter: No such file or directory' — permission-looking, but really a missing binary. And Windows CRLF endings append a carriage return to the interpreter path, so the kernel hunts for /bin/bash\r and reports it missing.

Diagnose with two commands: head -1 script.sh shows what the kernel sees, and file script.sh reports CRLF, missing newlines, and encoding. Resolve CRLF with dos2unix or a sed strip, and prefer #!/usr/bin/env bash over hard paths — env searches PATH, surviving across distros and containers. Verify the interpreter exists with command -v bash before blaming anything else.

Editors and Git cause most CRLF cases: a Windows checkout plus an over-eager autocrlf setting, or a file pasted through a web UI. Lock it down with .gitattributes (*.sh text eol=lf) so every clone lands Unix-clean regardless of platform. One attribute line prevents an entire category of 'works on my machine' exec failures.

fix-shebang.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# What does the kernel see on line 1? What does file(1) report?
# (CRLF, missing shebang, and bad paths all surface here)
head -1 scripts/deploy.sh
file scripts/deploy.sh

# Windows endings poison the interpreter path (/bin/bash\r not found)
# (strip carriage returns; then verify they're gone)
sed -i 's/\r$//' scripts/deploy.sh
file scripts/deploy.sh

# Portable shebang: env searches PATH instead of hard-coding /bin/bash
# (survives Ubuntu, NixOS, macOS, and minimal containers)
command -v bash
head -1 scripts/deploy.sh  # expect: #!/usr/bin/env bash

# Lock Unix endings for all shell scripts, every clone, every platform
# (add to .gitattributes at repo root, commit once)
printf '*.sh text eol=lf\n' >> .gitattributes
💡env shebang plus gitattributes ends it
#!/usr/bin/env bash survives every distro layout, and *.sh text eol=lf in .gitattributes stops CRLF at the clone. Two one-liners that retire the whole shebang category.
📊 Production Insight
A Windows-based contributor's PR added CRLF to three deploy scripts; Linux CI failed with 'bad interpreter' on all three. The team blamed the container image for an hour before file revealed the \r. The .gitattributes line landed the same day, and the category has never recurred — prevention beat diagnosis permanently.
🎯 Key Takeaway
Bit set but unrunnable means shebang or CRLF. Check head -1 and file, strip \r, use env-based shebangs, and enforce eol=lf in gitattributes.

Noexec Mounts and Read-Only Filesystems

When chmod +x won't stick or exec fails on a 755 file, the filesystem is vetoing execution. Mount option noexec blocks all exec on that mount — common on /tmp, removable media, and hardened partitions — regardless of bits. Read-only mounts (ro) block chmod itself, since mode changes are writes. Both produce permission-shaped errors with nothing wrong in the file's own metadata.

Confirm with mount | grep showing noexec on the path's filesystem, or findmnt -T . for the exact mount entry. df -h . names the device when the mount table is noisy. For noexec, the fixes are remount with exec (mount -o remount,exec, when policy allows) or relocate the script to an exec-allowed path like /usr/local/bin or the app directory. For ro, remount rw if appropriate — or accept read-only and run from a writable copy.

Never stage executables in /tmp on hardened hosts; it's noexec by policy in most CIS benchmarks. CI workspaces, artifact directories, and /opt/app are the honest homes. When containers are involved, check the volume mount flags too — a host noexec bind-mount vetoes exec inside the container identically. Document allowed exec paths in your hardening baseline so developers stop discovering them via failures.

check-mounts.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Which mount governs this path, and does it forbid exec?
# (noexec = bits irrelevant; ro = chmod itself fails as a write)
findmnt -T .
mount | grep -E 'noexec|ro,'
df -h .

# Remount with exec when policy allows (needs privileges)
# (otherwise relocate the script to an exec-allowed path)
sudo mount -o remount,exec /data
cp /tmp/staged-deploy.sh /usr/local/bin/deploy.sh
chmod 755 /usr/local/bin/deploy.sh

# Prove the new home permits exec before wiring it into automation
# (test -x plus a --help run beats another failed deploy)
test -x /usr/local/bin/deploy.sh && echo EXECUTABLE
/usr/local/bin/deploy.sh --help
📊 Production Insight
A cron job staged in /tmp ran for years until a hardening sprint mounted /tmp noexec fleet-wide — then every nightly backup 'permission denied' at once. The fix was a one-line path change to /opt/backup, but detection took two missed backup windows because nobody connected a mount change to an exec error. Infra changes need exec-path review.
🎯 Key Takeaway
Bits mean nothing under noexec; chmod means nothing under ro. Check findmnt, remount or relocate, and keep executables out of /tmp on hardened hosts.

Ownership, sudo, and Running as the Right User

Only the owner (or root) can chmod a file, so 'Operation not permitted' on chmod means someone else owns it — a root-created artifact, a coworker's copy, an extracted tarball. ls -l names the owner; either take ownership with chown (when policy allows) or have the owner set the bit. The immutable attribute (lsattr showing 'i') blocks even the owner until chattr -i clears it — rare, but unforgettable once seen.

Resist sudo ./script.sh as a reflex. It executes the entire script with full privileges, bypassing the ownership question instead of answering it, and any bug or malice inside now runs as root. Worse, files the script creates come out root-owned, seeding the next permission failure. Sudo is for identified privilege needs (binding port 80, writing /etc), granted to minimal commands — never a blanket around an unexamined script.

Run automation as dedicated service users with exactly the rights the job needs, and set ownership at install time (install -o app -m 755). Cron and systemd encode the user explicitly (crontab owner, User= in units), so the exec context is declared, not accidental. When the right user owns an executable file on an exec mount, the gate opens quietly every time.

fix-ownership.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Who owns it, and is it immutable? (only owner/root may chmod)
# ('Operation not permitted' on chmod always means look here)
ls -l scripts/deploy.sh
lsattr scripts/deploy.sh

# Take ownership when policy allows, or have the owner set the bit
# (then set the mode explicitly — never 777)
sudo chown "$USER" scripts/deploy.sh
sudo chattr -i scripts/deploy.sh
chmod 755 scripts/deploy.sh

# Install for service users with owner + mode declared together
# (cron/systemd then run a known-good context, not an accident)
sudo install -o appuser -g appuser -m 755 scripts/deploy.sh /opt/app/deploy.sh
sudo -u appuser /opt/app/deploy.sh --help
⚠ sudo executes the unknown as root
sudo ./script.sh runs every line with full privileges and creates root-owned files that break the next run. Fix ownership so the right user runs the code — reserve sudo for identified privilege needs, not exec refusals.
📊 Production Insight
During the outage, sudo attempts prompted for passwords the automation user lacked — burning 10 minutes and nearly triggering an account lockout. The eventual fleet fix was a mode change needing no privileges beyond the config tool's normal scope. Reaching for sudo first cost time and risked the deploy account.
🎯 Key Takeaway
chmod needs ownership; immutable needs chattr -i. Install with owner and mode declared, run services as dedicated users, and never sudo around an unexamined script.

Harden the Pipeline So Bits Survive Shipping

The incident's real bug lived in packaging, not on any host — so the permanent fix lives there too. Build tarballs with preserved modes (tar --preserve-permissions or -p on extract), publish zip artifacts with executable attrs intact, and gate CI with test -x over every file under scripts/ plus bash -n syntax checks. A one-line gate that fails the build on a missing bit converts 23-host outages into red pull requests.

Teach Git to carry the bit: chmod +x before the first commit recording, and review mode changes (old mode/new mode lines in diffs) as carefully as content changes. A PR that flips deploy.sh to 644 deserves the same scrutiny as one deleting a deploy step. Pair with .gitattributes eol=lf and an env shebang standard, and scripts arrive executable, Unix-clean, and portable on every clone.

Document the triage order where on-call eyes find it: bit (ls -l, test -x), interpreter (head -1, file), mount (findmnt, noexec?), ownership (ls -l, lsattr). Five commands, one minute, in likelihood order. Teams that print this on the deploy runbook stop losing Fridays to two-word errors. Review the runbook after every related incident so each outage sharpens the checklist instead of repeating it.

gate-executables.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# CI gate: every script executable AND syntactically valid
# (fails the build — never 23 hosts at 3 PM on Friday)
for f in scripts/*.sh; do
  test -x "$f" || { echo "NOT EXECUTABLE: $f"; exit 1; }
  bash -n "$f" || { echo "SYNTAX ERROR: $f"; exit 1; }
done
echo "all scripts executable and clean"

# Preserve modes through packaging (both directions)
# (create with modes stored; extract with modes honored)
tar --preserve-permissions -czf release.tar.gz scripts/
tar -xzf release.tar.gz --preserve-permissions
ls -l scripts/

# On-call triage in likelihood order: bit, interpreter, mount, owner
# (five commands, one minute, run before reading a single line of code)
ls -l scripts/deploy.sh; head -1 scripts/deploy.sh
file scripts/deploy.sh; findmnt -T scripts/; lsattr scripts/deploy.sh
📊 Production Insight
After adding the test -x gate, the same team caught two more bit-stripping regressions in CI within a quarter — a new archiver default and a Windows contributor's commit — both fixed as 5-minute PRs. The gate paid for its one line roughly a hundredfold in incident hours avoided.
🎯 Key Takeaway
Preserve modes in packaging, gate test -x plus bash -n in CI, track bits in Git review, and print the five-command triage on the runbook.
● Production incidentPOST-MORTEMseverity: high

Deploy Script Lost +x in the Tarball and Blocked Releases for 52 Minutes

Symptom
At 3:04 PM on release Friday, the deploy orchestrator pushed release 4.17.0 to 23 production hosts. On every host the same step failed: ./scripts/deploy.sh returned 'Permission denied' in under a second. Engineers re-ran the job three times, then tried sudo, which asked for passwords the automation user didn't have. Twenty-three hosts sat on the old release while the team debugged a script whose contents were provably correct — cat showed every line intact.
Assumption
The team assumed the script was broken or corrupted in transit, because 'the script works on my machine' — locally, ./scripts/deploy.sh ran fine. Nobody checked permission bits because the failure message pointed their eyes at the code, not the metadata. The tarball build step (tar -czf without -p, unpacked under a restrictive umask) had stripped the execute bit, so the shipped file was mode 644 everywhere while every dev checkout was 755.
Root cause
The CI packaging job created the release tarball without preserving modes, and the extraction umask on production hosts produced deploy.sh at 644 (rw-r--r--). The kernel's exec check requires the execute bit for the automation user, so ./scripts/deploy.sh failed on all 23 hosts identically. Content was byte-perfect; only the mode was wrong. Local checkouts worked because Git preserves the 755 bit it recorded when the script was first committed with +x.
Fix
The on-call engineer ran chmod +x scripts/deploy.sh across the fleet with the config-management tool, and the fourth deploy attempt went green in 9 minutes. The permanent fix added a CI gate asserting test -x on every file under scripts/ before packaging, plus tar --preserve-permissions in the build step. The team also added bash -n syntax validation so future packaging failures surface in CI instead of on 23 hosts at 3 PM.
Key lesson
  • Ship modes, not just bytes. Packaging pipelines must preserve and verify execute bits — a test -x gate in CI costs one line and would have caught this before a single host saw the tarball. Treat file metadata as release content.
  • Read 'Permission denied' as metadata first, code second. ls -l plus test -x takes five seconds and answers the most common cause immediately. Debugging script contents before checking the bit burns incident minutes on the wrong layer.
  • Never sudo past an exec refusal you don't understand. Passwordless sudo would have 'fixed' this by executing as root — hiding the packaging bug and granting a release script full privileges it never needed.
Production debug guideFive gates that produce the same two words, ordered by likelihood, each with the confirming check and the fix.5 entries
Symptom · 01
./script.sh says 'Permission denied' right after creating or downloading it
→
Fix
Run ls -l script.sh. If the mode shows rw-r--r-- (644) with no x anywhere, that's the whole story: chmod +x script.sh, then re-run. Verify with test -x script.sh && echo executable. This covers fresh files, curl downloads, and editor-created scripts — nothing is born executable except via explicit chmod or version control.
Symptom · 02
The bit is set (755) but execution still fails
→
Fix
Check the shebang with head -1 script.sh and file script.sh. A missing shebang, a wrong path like #!/bin/bash on a system with bash elsewhere, or Windows CRLF line endings (file reports 'with CRLF line terminators') all break exec. Fix the interpreter path via command -v bash, or convert endings with dos2unix or sed -i 's/\r$//'. Confirm with ./script.sh after.
Symptom · 03
chmod +x succeeds but the bit vanishes or exec still fails
→
Fix
Run mount | grep <filesystem> and look for noexec, or df -h . plus touch tests for read-only mounts. Remedies: remount with exec (mount -o remount,exec) if policy allows, or move the script to an exec-allowed path like /usr/local/bin. /tmp is noexec on many hardened hosts — never stage executables there.
Symptom · 04
You can't chmod the file: 'Operation not permitted'
→
Fix
Run ls -l to check ownership plus lsattr for immutable flags. If another user owns it, either take ownership (sudo chown $USER file, if policy allows) or have the owner chmod it. If lsattr shows 'i', clear it with chattr -i first. Don't sudo-execute around ownership problems — fix the ownership so the right user runs the right code.
Symptom · 05
The script runs but a command inside it gets 'Permission denied'
→
Fix
The failure moved one layer down: a helper binary, a data file, or a directory traversal lacks rights. Run bash -x script.sh to find the exact failing line, then ls -l that path and namei -l for directory traversal. Fix the inner path's bit or ownership — re-chmodding the outer script changes nothing at this stage.
Permission Denied Causes — Confirm, Fix, Prevent
Root CauseHow to ConfirmFixPrevention
Missing execute bit (fresh file, download, stripped tarball)ls -l shows 644; test -x failschmod +x (or 755) and re-runCI gate with test -x; preserve modes in packaging
Bad shebang or CRLF line endingshead -1 wrong/missing; file reports CRLF terminatorsenv-based shebang; strip CR with sed/dos2unix.gitattributes eol=lf; standardize on #!/usr/bin/env bash
noexec or read-only mountfindmnt/mount shows noexec or ro on the pathRemount exec/rw or relocate to an allowed pathNever stage executables in /tmp on hardened hosts
Wrong ownership or immutable flagchmod says Operation not permitted; lsattr shows ichown to runner; chattr -i; install with owner+modeSet ownership at install time; run as dedicated users
Inner file or directory lacks rightsbash -x pinpoints a failing inner path; namei shows traversal gapFix the inner path's bit/ownership, not the outer scriptTest scripts as the service user in CI, not as root
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
bypass-vs-exec.shbash scripts/deploy.shRun via Bash vs Execute Directly
fix-shebang.shhead -1 scripts/deploy.shShebang and Line Endings
check-mounts.shfindmnt -T .Noexec Mounts and Read-Only Filesystems
fix-ownership.shls -l scripts/deploy.shOwnership, sudo, and Running as the Right User
gate-executables.shfor f in scripts/*.sh; doHarden the Pipeline So Bits Survive Shipping

Key takeaways

1
Triage metadata before code
ls -l, test -x, head -1, file, findmnt, lsattr — in that order.
2
Fresh files are 644. chmod 755 plus Git bit tracking keeps scripts runnable everywhere.
3
bash script.sh diagnoses; ./script.sh proves. Fix the gate so direct exec works for all callers.
4
env shebangs plus .gitattributes eol=lf retire interpreter and CRLF failures permanently.
5
noexec and ro mounts veto bits. Relocate executables; never stage them in /tmp on hardened hosts.
6
Gate test -x and bash -n in CI so packaging regressions fail builds, not Friday releases.

Common mistakes to avoid

6 patterns
×

Debugging script contents before checking the bit

Symptom
Twenty minutes re-reading correct code while ls -l would have shown 644 in five seconds, burning incident time on the wrong layer.
Fix
Triage metadata first: ls -l, test -x, then head -1 and file. Read code only after the gates check out.
×

Running sudo ./script.sh as a first resort

Symptom
Password prompts stall automation, root-owned outputs seed future failures, and unknown code runs with full privileges while the real bug hides.
Fix
Fix ownership and bits so the right user runs the code. Reserve sudo for identified privilege needs, never exec refusals.
×

chmod 777 to 'make it work'

Symptom
The script runs, but now anyone on the host can modify it — the next execution runs attacker's code with your user's rights.
Fix
Use 755 for executables, 644 for data. World-writable scripts are a vulnerability, not a fix.
×

Treating bash script.sh as the permanent fix

Symptom
Works in your terminal but breaks CI, cron, and teammates who invoke ./ — the bypass masked the gate instead of opening it.
Fix
Use the bypass once for diagnosis, then fix bit, shebang, or mount so direct execution works everywhere.
×

Staging executables in /tmp on hardened hosts

Symptom
Scripts that ran for years fail fleet-wide after a hardening change mounts /tmp noexec, with failures scattered across unrelated jobs.
Fix
Keep executables in /usr/local/bin, /opt/app, or CI workspaces. Review exec paths after every hardening change.
×

Shipping tarballs that strip modes

Symptom
Byte-perfect scripts fail on every target with 644 modes while dev checkouts work, stalling releases across the whole fleet at once.
Fix
Preserve modes in archiving, gate test -x plus bash -n in CI, and review mode flips in PR diffs like content changes.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
You get 'Permission denied' running ./deploy.sh. What's your first three...
Q02JUNIOR
bash script.sh works but ./script.sh fails. What does that prove?
Q03SENIOR
chmod +x succeeds but execution still fails on a 755 file. Next steps?
Q04SENIOR
Why is sudo ./script.sh a bad first resort for this error?
Q05SENIOR
How do you stop a release pipeline from shipping non-executable scripts?
Q01 of 05JUNIOR

You get 'Permission denied' running ./deploy.sh. What's your first three commands?

ANSWER
ls -l deploy.sh to check the execute bit, test -x deploy.sh to confirm the kernel's view, and head -1 plus file if the bit is set. That orders triage by likelihood: missing bit first, then shebang/CRLF, then mounts and ownership.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Why does a file I just created refuse to execute?
02
Is bash script.sh a real fix or a workaround?
03
What does 'bad interpreter: No such file or directory' mean?
04
Why does chmod say 'Operation not permitted'?
05
Can I execute scripts from /tmp?
06
Does chmod 777 fix this faster?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
✓ Verified
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
🔥

That's Linux. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
Git Remote Origin Already Exists Fix
14 / 19 · Linux
Next
SSH Host Key Verification Failed Fix
→