@sebastienrousseau/dotfiles
Version:
The Trusted Shell Platform — Universal dotfiles managed by Chezmoi. Features Bash & Zsh for macOS, Linux & WSL. Rust modern tooling & enterprise-grade security.
146 lines (110 loc) • 5.53 kB
Markdown
---
render_with_liquid: false
---
# 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`](../../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`](../../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:
```bash
# Sourced by <parent>.sh; inherits set -euo pipefail.
```
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:
```bash
# preamble:skip — opt-out for completion / init fragments.
```
The lint accepts these. Use sparingly.
## Adding a new exemption
When you need to mark a new file:
1. **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 ever `source`d, mark it.
2. **Pick the right marker.**
- Sourced library: `# Sourced by <caller>; inherits set -euo pipefail.`
- Completion / init fragment: `# preamble:skip — <why>.`
3. **Run the checker:**
```bash
./tools/ci/check-shell-preamble.sh path/to/file.sh
```
Exit 0 = lint passes.
4. **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.sh` and document it in this
page's "Bulk-sourced fragments" table above.
## CI integration
- **Pre-commit hook** (`config/pre-commit-config.yaml` →
`shell-preamble-check`): blocks `git commit` if 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-check` hook).
- `.github/workflows/reusable-shell-lint.yml` — CI invocation.
- Issue [#854](https://github.com/sebastienrousseau/dotfiles/issues/854).