Extracted from the per-agent profile files and the private ai-agents skill, where they were duplicated and environment-coupled: - discovery over guessing: walk the help tree, never invent flags, never hand-roll a script when a dedicated CLI exists - non-interactive invocation: explicit targets, the tool's own --wait - machine-readable output flags (json/compact/csv) vs. never filtering diagnostics - orient before starting: project instructions, git log, prior work records, and verifying TODOs against the code - script placement: dedicated version-controlled dir, symlinked into PATH - destructive/outward-facing command care; environment drift verification coding-dev.md and code-languages/bash.md now cross-reference it instead of carrying their own one-line versions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.7 KiB
name, description
| name | description |
|---|---|
| bash | Use this skill when writing or modifying Bash/shell scripts, build pipelines, release scripts, or when running shell commands as an agent. Covers strict mode, shellcheck, quoting, the no-wait-loops rule, build-script structure, and destructive-command safety. |
Bash / Shell — Rules & Preferences
Extends ../coding-dev.md. Everything there applies; this adds shell specifics.
Bash is for orchestration — sequencing tools, wiring a build pipeline, gluing CLIs. Once a script grows real data structures, arithmetic, or parsing, it wants to be Python or a compiled tool instead.
1. Strict Mode & Linting
Every script starts with:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
-eexit on error,-uerror on unset variable,-o pipefailso a failure mid-pipe is not masked by a successful final command.shellcheckmust be clean and belongs in the build gate. Do not silence a finding with a# shellcheck disable=without a comment saying why.- Format with
shfmt.
Important
set -edoes not fire insideif,&&,||, or command substitution used in a condition. Do not assume a failing command inside those aborts the script — check explicitly where it matters.
2. Quoting & Expansion
- Quote every expansion:
"$var","${arr[@]}","$(cmd)". Unquoted expansion word-splits and glob-expands, which is how a path with a space becomes two broken arguments. "${var:-default}"for defaults;"${var:?message}"for required values.- Use arrays for argument lists — never build a command as a string and
evalit. [[ ]]over[ ].$(…)over backticks.localevery variable inside a function.printfoverechofor anything with escapes or leading dashes.
3. Never Write Wait Loops
Caution
Do not write
until <check>; do sleep 2; doneorwhile ! <check>; do sleep …; done. They hang the terminal, and the only way out is an interrupt.
To wait for something:
- Run the work in the background and wait for its completion signal.
- To check external state, take a single sample per invocation rather than looping.
- If a tool has a real blocking wait (
--wait,wait-for,systemctl is-active --wait), use that instead of polling around it.
This mirrors the general events-over-polling preference: prefer a notification to a loop.
4. Never Filter Build Output
Repeating the global rule because shell is where it gets violated:
# WRONG
cargo build 2>&1 | grep -E "error|warning"
npm run build | tail -20
# RIGHT
cargo build
npm run build
Tee to a log if you need a copy — cmd 2>&1 | tee build.log preserves the full output — but
never grep/head/tail it away. Narrow the command instead when output is genuinely huge.
5. Destructive Commands
- Look at the target before deleting or overwriting it.
lsit,statit, read it. - Never
rm -rf "$VAR"without first asserting$VARis non-empty and is what you think:[[ -n "${BUILD_DIR:-}" && -d "$BUILD_DIR" ]] || { echo "refusing: BUILD_DIR unset" >&2; exit 1; } - A shared output directory belongs to nobody. If two scripts write to
dist/, neither mayrm -rfit — each cleans only its own outputs. - Write scratch files to a
mktemp -dand trap cleanup:tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT - Prefer a
--dry-runflag on anything that deploys, releases, or deletes — and implement it honestly.
pgrep -f matches its own shell
Warning
The command line containing the pattern is itself a process, so
pgrep -f foo.confmatches the shell running it. Piping that intokillkills the running script — which reads exactly like "the thing I was testing never happened".Write the pattern so it cannot match itself:
pgrep -f 'my[a]pp\.conf'
6. Build Script Structure
A build script is the project's quality gate, and it should be exhaustive enough to catch mistakes before a human does.
- Fixed action order regardless of flag order, with any failure aborting the rest:
bump → lint → build → artifacts → stop → deploy → start → commit → push → release - A skipped test fails the gate. Never let a skip report green.
- Print a summary with numbers, not just a verdict — counts of warnings, suppressions, and file-size violations. A suppression count that has grown is worth seeing even on a pass.
- Keep per-step logs in a known directory so a failure can be read after the fact.
- Never bypass a failed gate by copying a binary or running the install step by hand. Fix the failing step; do not weaken the check to make it pass.
- Support
--dry-runfor the full release path. - Throttle heavy builds. Detect when something latency-sensitive is running and drop to
half the cores at
nice -n 10, announcing that it did so. Provide an override env var.
7. Paths & Portability
- Resolve the script's own directory rather than assuming the caller's
cwd:script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" - Prefer absolute paths in anything non-interactive.
- Entering a container resets the working directory — chain
cdinside the same command string:container-run -- bash -c "cd /path && make". - Don't hardcode
/tmpwhen the content must survive a cleanup;/var/tmppersists longer. command -v tool >/dev/null || { echo "tool required" >&2; exit 1; }before using an optional dependency. A silently skipped step is worse than a failure — a packaging step that quietly skips when its tool is missing will ship an incomplete release.
8. Secrets
- Read secrets from an environment file or secret store; never inline them in a script.
- Never
echoa secret, and never pass one as a command-line argument — argv is world-readable in the process table. Use an env var or stdin. - Never commit a file containing real credentials; commit an example with empty values.
9. Script Placement
Standalone utility scripts belong in a dedicated, version-controlled scripts directory,
symlinked into PATH rather than copied there. Never write a loose script directly into
~/.local/bin or another PATH directory — the source of truth should be one versioned
location. See ../cli-tooling.md §5 for the reasoning and the pattern.
10. Related
../coding-dev.md— the general rules this extends.../cli-tooling.md— invoking other tools from these scripts: discovery, non-interactive flags, structured output.python.md— where a shell script should have become a Python script.