Shebang and Script Permissions

Every Bash script begins with a special first line called the shebang, and every script needs the correct file permissions before Linux will let you run it directly. Together, these two things control which program interprets your script’s text and whether you’re even allowed to execute it. Get either one wrong and you’ll see a confusing Permission denied or No such file or directory error before a single command in your script runs. This lesson explains exactly what the shebang does, how the kernel uses it, and how permissions determine who can execute a script.

Overview / How It Works

A shebang (also called a “hashbang”) is the first line of a script, and it must start with the two characters #! followed by the absolute path to an interpreter, for example #!/usr/bin/env bash. This is not a Bash feature — it’s a feature of the Linux kernel itself. When you run a file directly (like ./disk-report.sh), your shell calls the execve() system call on that file. The kernel opens the file and inspects its first two bytes. If they are #!, the kernel treats the rest of that line as “the real program to run,” reads the interpreter path (and, optionally, a single argument after it), and re-executes that program instead, appending your script’s path as an argument. So #!/usr/bin/env bash actually causes the kernel to run /usr/bin/env with the argument bash, and env then searches your $PATH for a program named bash and executes it, handing it your script to interpret.

This matters because the shebang line is only consulted when the kernel is asked to execute the file directly. If you instead run bash disk-report.sh, you’re manually invoking the Bash interpreter and telling it to read the file as a script — the kernel never has to figure out the interpreter, so the shebang line is simply ignored (Bash treats it as an ordinary comment, since it starts with #).

Permissions are the second half of the story. Every file on a Linux system has three permission classes — owner, group, and other — and three permission types for each class: read (r), write (w), and execute (x). Running ls -l shows this as a ten-character string like -rwxr-xr-x: the leading - means “regular file,” then three groups of rwx for owner, group, and other. For a script, the execute bit is what tells the kernel “you’re allowed to run this file as a program.” Without it, attempting ./disk-report.sh fails at the kernel level with an access error — the shell reports it as Permission denied, and this happens regardless of how correct your shebang line is. Note that execute permission is about launching the file directly; the interpreter itself only needs read permission on the script to open and parse it once it’s running.

Syntax

The general shebang form is a comment-like line with a mandatory path and one optional argument:

#!<path-to-interpreter> [optional-single-argument]
Shebang Meaning
#!/bin/bash Always use the Bash binary installed at exactly /bin/bash.
#!/usr/bin/env bash Search $PATH for the first bash found and use that. More portable across systems where Bash isn’t at a fixed path.
#!/bin/sh Use the system’s POSIX shell (may be dash, not Bash — avoid Bash-only syntax if you use this).

To grant execute permission, use chmod:

chmod <mode> <file>
Symbolic mode Meaning
chmod +x file Add execute permission for owner, group, and other alike.
chmod u+x file Add execute permission for the owner only.
chmod 755 file Set owner to read/write/execute (7), group and other to read/execute (5) — identical result to chmod u=rwx,g=rx,o=rx file.
chmod -x file Remove execute permission for everyone.

Examples

Example 1: Writing, then executing, a script

Save the following as disk-report.sh:

#!/usr/bin/env bash

report_dir="/var/log/disk-reports"
mkdir -p "$report_dir"

df -h / > "$report_dir/report-$(date +%F).txt"
echo "Report saved to $report_dir"

If you try to run it right after creating it, before setting any permissions, you’ll hit a wall:

$ ./disk-report.sh
bash: ./disk-report.sh: Permission denied

Output: the file has no execute bit set yet, so the kernel refuses to run it as a program, no matter how correct the shebang line is. Fix it with chmod, then run it:

ls -l disk-report.sh
chmod +x disk-report.sh
ls -l disk-report.sh
./disk-report.sh

Output:

-rw-r--r-- 1 alice alice 142 Aug  4 10:02 disk-report.sh
-rwxr-xr-x 1 alice alice 142 Aug  4 10:02 disk-report.sh
Report saved to /var/log/disk-reports

The first ls -l shows no x anywhere in the permission string; after chmod +x, the owner, group, and other classes all gain execute permission, and ./disk-report.sh runs successfully.

Example 2: Hardcoded path vs. env lookup

These two scripts behave differently in how they find Bash:

#!/bin/bash
echo "Using the bash located at /bin/bash"
#!/usr/bin/env bash
echo "Using whichever bash the PATH finds first"

Check where your shell actually finds Bash:

which bash

Output:

/usr/bin/bash

On most Debian/Ubuntu and RHEL/Fedora systems both approaches resolve to the same binary, so it rarely matters day to day. The difference shows up on systems where Bash lives somewhere unusual (some macOS setups, Nix, or containers with a custom-built Bash in a non-standard prefix) — #!/bin/bash would fail with “No such file or directory” there, while #!/usr/bin/env bash still finds it via $PATH.

Example 3: A shebang that isn’t really a shebang

The shebang only works as an interpreter directive if it is the very first line — byte one, character one — of the file. A leading comment or blank line silently disables it:

# my backup script
#!/usr/bin/env bash
echo "This will NOT be treated as a shebang, because it's not on line 1"

Here the kernel sees # my backup script as the first line — not #! — so it doesn’t recognize the file as having an interpreter directive at all. Depending on your shell, this typically falls back to running the file with /bin/sh instead of Bash, which can break anything using Bash-only syntax like [[ ]] or arrays. The fix is simply to put the shebang first:

#!/usr/bin/env bash
# my backup script
echo "This works correctly, because the shebang is line 1"

How It Works Step by Step

When you type ./disk-report.sh at a Bash prompt, here’s what actually happens:

  • Your interactive Bash forks a child process.
  • The child calls execve("./disk-report.sh", ...).
  • The kernel opens the file and checks whether the execute bit is set for you (owner/group/other, whichever applies). If not, execve fails immediately with EACCES, which Bash reports as Permission denied.
  • If execute permission is granted, the kernel reads the first bytes of the file. Seeing #!, it parses the rest of the line as an interpreter path plus an optional single argument.
  • The kernel re-invokes execve, this time on the interpreter (e.g. /usr/bin/env), passing it the optional argument (bash) followed by the original script’s path.
  • env searches $PATH, finds bash, and execs it in turn, still carrying the script’s path as an argument.
  • Bash opens the script file (needing only read permission at this point), reads it line by line, and executes each command in order.
  • When the script finishes, its exit status becomes available in $? in the parent shell.

Common Mistakes

Forgetting to make the script executable

The single most common error for beginners — trying to run a freshly-written script with ./script.sh before granting execute permission.

$ ./disk-report.sh
bash: ./disk-report.sh: Permission denied

Fix it with chmod +x, or, as a one-off workaround, invoke the interpreter directly and skip the execute-bit requirement entirely (this only needs read permission on the file):

bash disk-report.sh

Putting anything before the shebang

A blank line, a comment, or even a stray space before #! means the kernel no longer treats it as an interpreter directive, silently changing which shell actually runs your script (see Example 3 above). Always make the shebang the literal first line of the file.

Windows-style line endings (CRLF)

If a script was edited on Windows and saved with CRLF line endings, the shebang line ends in an invisible carriage-return character. The kernel includes that character as part of the interpreter path it looks up, and the lookup fails:

$ ./disk-report.sh
bash: ./disk-report.sh: /usr/bin/env: bash^M: No such file or directory

The ^M represents the stray \r character. Fix it by converting the file to Unix line endings:

sed -i 's/\r$//' disk-report.sh

Best Practices

  • Always put the shebang on line 1, with no blank lines, comments, or whitespace above it.
  • Prefer #!/usr/bin/env bash for portability, unless you specifically need to pin to the system’s fixed /bin/bash (for example, in a security-sensitive script where you don’t trust the caller’s $PATH).
  • Run chmod +x on a script as soon as you intend for people to run it with ./script.sh.
  • Use ls -l to double-check permissions when a script mysteriously won’t run.
  • Configure your editor to save scripts with Unix (LF) line endings, not Windows (CRLF).
  • Remember that execute permission is not required to source a script (source script.sh or . script.sh) — sourcing just needs read permission, since it runs the commands in your current shell rather than executing the file as a separate program.
  • Keep the shebang’s optional argument to a single word — the kernel only supports one argument after the interpreter path on most Linux systems.

Practice Exercises

  • Write a script called uptime-check.sh that prints the output of the uptime command. Save it with #!/usr/bin/env bash as the first line, try running it with ./uptime-check.sh before setting any permissions, observe the error, then fix it with chmod.
  • Take a working script and deliberately move its shebang line down to line 2 (add a comment above it). Run the script and observe what changes. Hint: use ls -l /bin/sh to see what your system’s /bin/sh actually points to.
  • Create a script with chmod 644 permissions (no execute bit) and try three ways to run it: ./script.sh, bash script.sh, and source script.sh. Note which ones succeed and why.

Summary

  • The shebang (#!) must be the very first two bytes of a script; it tells the kernel which interpreter to re-exec with.
  • #!/usr/bin/env bash searches $PATH for Bash and is generally more portable than hardcoding #!/bin/bash.
  • The shebang is only used when the kernel executes the file directly (./script.sh); running bash script.sh bypasses it entirely.
  • The execute permission bit (x), set with chmod +x, is what allows a file to be run directly as a program — separate from read permission, which is all an interpreter needs to read the script’s contents.
  • A shebang not on line 1, or corrupted by Windows CRLF line endings, silently breaks interpreter selection and produces confusing errors.