clean-shell · v1.5 · 2026-10-23 · sha256 535583d2e7fdfd65
clean-shell v1.5A
Immutable. This exact content never changes and is served at /api/v1/blob/535583d2e7fdfd65.
---
name: clean-shell
description: "Defensive Bash and POSIX shell rules: strict-mode flags, traps, idempotent mutation, ShellCheck discipline, bats tests."
when_to_use: "Use when writing, hardening, reviewing, or testing shell scripts, or wiring shellcheck and bats into CI."
metadata:
author: uwuclxdy
version: "1.5"
---
# Clean Shell
Shell-specific conventions for writing, hardening, reviewing, and testing scripts. The core rules below always apply. Load the one reference file matching the task; don't load them all.
| Task touches | File |
|---|---|
| Scripts that run as root or mutate live systems: deploy/apply, firewall/sshd/sudoers edits, systemd oneshot+timer units, ssh remote-exec, rollback, cleanup traps, idempotency, parsing input a foreign process controls (`/proc`, argv, filenames) | `references/defensive.md` |
| `.shellcheckrc`, `# shellcheck disable=` directives, severity floors, exit codes, CI gating | `references/shellcheck.md` |
| Bats tests: `.bats` files, `run`, `setup`/`teardown`, PATH-stub mocking, parallel jobs | `references/bats.md` |
## Core Rules
- Pick `set` flags by intent and comment the reason next to them: `set -euo pipefail` for orchestration where any failure aborts; drop `-e` when steps may fail without aborting; `set -u` alone for long-running loops. A thin wrapper sets no flags and ends with `exec`.
- Quote every expansion (`"$var"`, `"${arr[@]}"`, `"$(cmd)"`); `--` end-of-options guard before untrusted operands.
- Validate input at the boundary into a checked value (`"${1:?msg}"`, a `case` guard `*[!0123456789]*|''`, since a locale's `[0-9]` can match other digits); no call site re-tests a raw string. Arithmetic executes text it evaluates (`a[$(cmd)]` runs `cmd`): `$(( ))`, `(( ))`, `let`, `[[ -eq/-lt/... ]]`, an indexed-array subscript, a `${s:off:len}` offset, and any assignment to a `declare -i`/`local -i` variable; anything reaching one gets, in order: the guard; a leading-zero strip (`v=${v#"${v%%[!0]*}"}; v=${v:-0}`; octal in sh and bash: `08` errors, `010` is 8); a digit count (at most 18) before any arithmetic, `10#` included (19+ digits can overflow silently, comparisons too); the domain's range check. `references/defensive.md` holds the code and the 19-digit case.
- Text another process controls is untrusted input even when a local file carries it (`/proc/*/comm`, `/proc/*/cmdline`, filenames); strip every delimiter you later split on.
- Every `mktemp` gets an EXIT trap right after creation. Traps are best-effort: SIGKILL, OOM-kill, and power loss skip them.
- Anything that mutates system state is check-then-act idempotent; add `flock` where concurrent runs are possible.
- Never echo a generated secret; `set -x` tracing and `ps`/`/proc/*/cmdline` argv leak it too.
- Every `# shellcheck disable=` carries a same-line reason comment.
- Non-trivial scripts get bats tests covering the error paths, not just the happy path.