Files
skills-public/code-languages/bash.md
T
BluscreamandClaude Opus 5 72834167b6 feat(cli-tooling): add generic CLI, tooling and script-placement rules
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>
2026-09-18 12:40:55 +02:00

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'
  • -e exit on error, -u error on unset variable, -o pipefail so a failure mid-pipe is not masked by a successful final command.
  • shellcheck must 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 -e does not fire inside if, &&, ||, 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 eval it.
  • [[ ]] over [ ]. $(…) over backticks.
  • local every variable inside a function.
  • printf over echo for anything with escapes or leading dashes.

3. Never Write Wait Loops

Caution

Do not write until <check>; do sleep 2; done or while ! <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. ls it, stat it, read it.
  • Never rm -rf "$VAR" without first asserting $VAR is 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 may rm -rf it — each cleans only its own outputs.
  • Write scratch files to a mktemp -d and trap cleanup:
    tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
    
  • Prefer a --dry-run flag 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.conf matches the shell running it. Piping that into kill kills 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-run for 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 cd inside the same command string: container-run -- bash -c "cd /path && make".
  • Don't hardcode /tmp when the content must survive a cleanup; /var/tmp persists 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 echo a 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.


  • ../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.