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.
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 — and that header is only the beginning.
This note covers the strict-mode header, cleanup traps, quoting rules, and the blind spots -e does not catch. If you run scripts as root or in CI, pair this with ShellCheck in the pipeline. For a full ops scripting track, see Bash for ops, done safely.
Strict mode catches runtime failures; ShellCheck catches review-time mistakes. Use both.
bash deploy.sh # no strict modedeploy.sh: line 4: cd: /srv/relese: No such file or directoryrm -rf * then ran in $HOME — the typo did not stop the script bash deploy.sh # with set -euo pipefaildeploy.sh: line 4: cd: /srv/relese: No such file or directoryaborted at line 4 — nothing destructive ranThe 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 command output. The three flags are not interchangeable: -e exits on failure, -u treats unset variables as errors, and -o pipefail means a failed stage in a pipeline fails the whole pipe.
#!/usr/bin/env bashset -euo pipefailIFS=$'\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 cannot live at the bottom — it will 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. Never use /tmp/myapp — use mktemp -d and let the kernel assign a unique directory.
tmp="$(mktemp -d)"trap 'rm -rf "$tmp"' EXIT # always runs, however we leavecurl -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. This is the difference between deleting one file and deleting everything in a directory because $path was empty. The same rule applies inside $(...) command substitutions — quote the inner variables too.
For scripts that accept user input, validate before use. Strict mode stops your typos; it does not stop an attacker from passing ; rm -rf / if you eval unchecked input. Treat every external value as untrusted even when -u is on.
# wrong — word-splits and globs on the contents of $pathrm -rf $path/*# right — quote scalars, use an array for arg listsrm -rf "$path"/*args=(--config "app prod.conf" --force)mycmd "${args[@]}"
Where this goes next
Make the machine enforce it: run shellcheck in CI and it will 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. The Bash for ops, done safely course covers traps, quoting, and testing patterns for scripts that run as root.