BlogLinux & scripting

Bash strict mode: writing ops scripts that fail loudly

set -euo pipefail is the start, not the whole story. Traps, quoting, and safe temp files for scripts that run as root.

Oct 28, 2025·6 min readBeginner

A Bash script that plows on after an error is how a backup job 'succeeds' every night while shipping an empty tarball. By default Bash ignores failures, treats unset variables as empty strings, and reports a pipeline as successful if the last command worked. Three flags flip all of that so your scripts fail loudly instead of failing silently.

The difference is not subtle — a typo'd cd that keeps going can run a rm -rf in the wrong directory:

bash — strict vs laxlive
bash deploy.sh # no strict mode
deploy.sh: line 4: cd: /srv/relese: No such file or directory
rm -rf * then ran in $HOME — the typo did not stop the script
 
bash deploy.sh # with set -euo pipefail
deploy.sh: line 4: cd: /srv/relese: No such file or directory
aborted at line 4 — nothing destructive ran
One header changes everything
Default bash
Errors are ignored
Unset var = empty string
Pipe status = last stage only
Ships broken, silently
set -euo pipefail
Aborts on the first error
Unset var is a hard error
Any pipe stage fails the pipe
Fails loud, fails safe

The header, line by line

Put this at the top of every script. Setting IFS to just newline and tab stops the other classic footgun — word-splitting on spaces when you loop over output.

strict.sh
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# -e exit the moment any command fails
# -u unset variable is an error, not ""
# -o pipefail a failed stage fails the whole pipeline

Clean up with traps

-e means your script can exit at any line, so cleanup can't live at the bottom — it'll get skipped. A trap on EXIT runs on success, on error, and on Ctrl-C alike. Pair it with mktemp so you never hardcode a temp path two scripts can collide on.

strict.sh
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT # always runs, however we leave
curl -fsSL "$URL" -o "$tmp/archive.tgz"
tar -xzf "$tmp/archive.tgz" -C "$tmp"

Quote everything

An unquoted variable is split on whitespace and glob-expanded before the command ever sees it. Quote every expansion, and use arrays — not space-joined strings — whenever you build an argument list.

quoting.sh
# wrong — word-splits and globs on the contents of $path
rm -rf $path/*
# right — quote scalars, use an array for arg lists
rm -rf "$path"/*
args=(--config "app prod.conf" --force)
mycmd "${args[@]}"
set -e has blind spots
It will not fire inside an if/while condition, on either side of && / ||, or in a command substitution without shopt -s inherit_errexit. For the commands that must not fail, check them explicitly rather than trusting -e to catch it.

Where this goes next

Make the machine enforce it: run shellcheck in CI and it'll flag the unquoted variables, the useless cats, and the [ ] tests that should be [[ ]] before they reach main. Strict mode catches failures at runtime; ShellCheck catches them at review.

Go deeper in a courseBash for ops, done safelyStrict mode, traps, quoting and ShellCheck — scripts that fail loudly.View course

Related posts