Shell Preamble Exemptions¶
Every bash script in this repo must include set -euo pipefail (or
set -eu for POSIX sh) within its first 50 lines. The rule is
enforced by
tools/ci/check-shell-preamble.sh,
wired into pre-commit and the reusable-shell-lint.yml CI job.
Tests pin the contract under
tests/unit/security/test_shell_preamble_lint.sh.
Why the preamble is mandatory¶
set -e fails the script on the first error. set -u rejects
reads of unset variables. set -o pipefail propagates the exit
code of the last failing command in a pipeline rather than only the
final command's. Together they convert silent failures into loud
ones — which is the only way to catch them.
Real example from this repo's history: executable_dot-load-benchmark
shipped with only set -e. A failing pipeline (e.g. cmd | grep)
where grep succeeded would still report success even when cmd
crashed. The full triple catches this. The benchmark was upgraded
in commit 970b631d under #854.
Exemption categories¶
Two kinds of files legitimately can't enforce the preamble locally:
1. Sourced libraries¶
Files loaded via source / . into a caller's shell context. The
caller already has set -euo pipefail; the library inherits it.
Adding set -e locally would persist after the library returns and
break the caller's own error-handling logic.
These files must carry a comment header in their first 15 lines:
The lint accepts files with this marker regardless of preamble state. Current adopters:
| Path | Sourced by |
|---|---|
dot_config/shell/00-core-paths.sh.tmpl |
shell init (zsh/bash/fish) |
dot_config/shell/05-core-safety.sh |
shell init |
dot_config/shell/10-secrets.sh |
shell init |
dot_config/shell/40-fzf-defaults.sh.tmpl |
shell init |
dot_config/shell/40-ls-colors.sh |
shell init |
dot_config/shell/50-logic-functions-core.sh.tmpl |
shell init + fish bridge |
dot_config/shell/50-logic-functions.sh.tmpl |
shell init + fish bridge |
dot_config/shell/51-logic-functions-extra.sh.tmpl |
shell init (lazy) |
dot_config/shell/90-ux-aliases.sh.tmpl |
shell init + fish bridge |
dot_config/shell/91-ux-aliases-lazy.sh.tmpl |
shell init (lazy) |
scripts/dot/lib/bento.sh |
dot CLI commands |
scripts/dot/lib/log.sh |
dot CLI + diagnostics |
scripts/dot/lib/platform.sh |
dot CLI commands |
scripts/dot/lib/ui.sh |
dot CLI + diagnostics + ops |
scripts/dot/lib/utils.sh |
dot CLI |
scripts/ops/heal-chezmoi.sh |
scripts/ops/heal.sh |
scripts/ops/heal-system.sh |
scripts/ops/heal.sh |
scripts/ops/heal-tools.sh |
scripts/ops/heal.sh |
2. Bulk-sourced fragments (path-skipped)¶
Three directories contain hundreds of alias / function / PATH snippets that are sourced into the shell. Annotating each one individually would be 300+ marker comments with no extra signal. The lint skips them by path:
| Path | Contents |
|---|---|
.chezmoitemplates/aliases/**/*.aliases.sh |
alias definitions sourced into the user's shell. |
.chezmoitemplates/functions/** |
shell-function definitions sourced into the user's shell. |
.chezmoitemplates/paths/** |
PATH=…:$PATH snippets concatenated by the shell init. |
3. Test scripts (path-skipped)¶
tests/** — every file under the test tree is invoked through
tests/framework/test_runner.sh which manages shell options for its
children. The runner itself has the full preamble; children that
source the framework inherit it.
4. Explicit opt-outs¶
Init fragments and completion scripts that must NOT carry their own
preamble (e.g., dot_local/bin/executable_dot_completion, a zsh
completion sourced into the interactive shell) can carry an
explicit marker:
The lint accepts these. Use sparingly.
Adding a new exemption¶
When you need to mark a new file:
-
Decide whether it's actually sourced or executed. If a user ever invokes it directly (chmod +x +
./file.sh), add the preamble. If it's only eversourced, mark it. -
Pick the right marker.
- Sourced library:
# Sourced by <caller>; inherits set -euo pipefail. -
Completion / init fragment:
# preamble:skip — <why>. -
Run the checker:
Exit 0 = lint passes.
- If you're adding a new entire directory of fragments
(rare — alias buckets, plugin trees), edit the path-skip rule
in
tools/ci/check-shell-preamble.shand document it in this page's "Bulk-sourced fragments" table above.
CI integration¶
- Pre-commit hook (
config/pre-commit-config.yaml→shell-preamble-check): blocksgit commitif a staged shell file fails the lint. - CI (
.github/workflows/reusable-shell-lint.yml): runs the full-repo scan on every PR.
A change to either the checker or the marker convention must update this page in the same PR.
References¶
tools/ci/check-shell-preamble.sh— the lint.tests/unit/security/test_shell_preamble_lint.sh— contract test.config/pre-commit-config.yaml(shell-preamble-checkhook)..github/workflows/reusable-shell-lint.yml— CI invocation.- Issue #854.