# Sota Shell Scripting

> State-of-the-art shell scripting (bash-focused, defensive) for writing and auditing shell scripts, CI scripts, init/deploy scripts, container entrypoints, and Makefile recipes — and, just as much, for the ad-hoc commands you run yourself: a grep/find/rg sweep whose result you are about to report, a one-liner pasted from a checklist, a pipeline whose exit status or empty output you are about to believe. Use when creating, modifying, reviewing or hardening shell code, AND before trusting any conclusion a shell command produced — especially an ABSENCE ("no matches", "nothing found", "0 results"), which a quoting bug produces identically. Trigger keywords: bash, shell script, sh, zsh, shellcheck, shfmt, CI script, Makefile shell, entrypoint script, set -euo pipefail, dotfiles, install script, cron job, wrapper script, one-liner, command line, grep sweep, search the codebase, verify a claim, no matches found, empty output, exit status, word splitting, glob.

- Skill: `martinholovsky/sota-shell-scripting` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add martinholovsky/sota-shell-scripting`
- Raw SKILL.md: https://api.skillmd.com/api/skills/martinholovsky/sota-shell-scripting/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: martinholovsky (https://skillmd.com/u/martinholovsky)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/martinholovsky/sota-shell-scripting

---


# SOTA Shell Scripting

Purpose: produce shell scripts that survive contact with reality — unusual filenames, missing
commands, partial failures, hostile input, signals, and concurrent invocation — and audit
existing scripts for the defect classes that cause most production shell incidents:
unquoted expansions, silent error swallowing, injection, secret leakage, and temp-file races.

Bash-focused (bash 5.x current; macOS ships bash 3.2 and defaults to zsh — see portability
rules). POSIX `sh` only when the target demands it (busybox/dash containers, init systems).

## First decision: should this be shell at all?

**Do NOT use shell when any of these hold.** Recommend Python/Go (or the project's primary
language) instead, and say so explicitly in BUILD and AUDIT output:

- Script exceeds ~100 lines of actual logic (not counting boilerplate/usage text).
- Needs real data structures (nested maps, JSON manipulation beyond a `jq` one-liner, sets).
- Needs granular error handling (retry *this* step, distinguish error kinds, partial rollback).
- Does arithmetic beyond integers, date math, or float comparison.
- Parses structured formats (JSON/YAML/XML) with string surgery instead of `jq`/`yq`.
- Needs portable concurrency beyond "run N jobs and `wait`".
- Is security-critical input handling (auth, parsing untrusted network data).

Shell is the right tool for: gluing processes together, CI steps, container entrypoints,
small install/deploy wrappers, environment setup — anything that is mostly *invoking other
programs* rather than computing.

## BUILD mode

When writing or modifying shell scripts:

1. **Pick the dialect deliberately.** `#!/usr/bin/env bash` unless the target is a minimal
   container/init context that only guarantees POSIX `sh`. Never `#!/bin/sh` with bashisms.
2. **Start every bash script from the safety preamble** (rules/01): `set -euo pipefail`,
   trap-based cleanup, `IFS` discipline — and know where `set -e` does NOT fire.
3. **Quote every expansion.** `"$var"`, `"$@"`, `"$(cmd)"`. Build commands with arrays,
   never with string concatenation.
4. **Errors are loud and routed to stderr** with script name + context; exit codes are
   meaningful and documented in `--help`.
5. **Make it idempotent and interrupt-safe**: `mktemp` + `trap` cleanup, atomic writes via
   `mv`, check-before-create, `flock` if concurrent runs are possible.
6. **Run `shellcheck` (treat all findings as blockers, suppress only with a justifying
   comment) and `shfmt -d` before declaring done.** If they are unavailable locally, state
   that and flag CI must run them.
7. Provide `--help` always; `--version` for distributed tools; `set -x` behind a
   `DEBUG`/`TRACE` env guard, never unconditionally (secret leakage).

## AUDIT mode

When reviewing existing shell scripts, hunt the defect classes in rules/ files bottom-up
(each rules file ends with an audit checklist of grep patterns and ShellCheck codes).
Run `shellcheck -S style` on every script if available; correlate findings with context —
ShellCheck flags symptoms, you judge exploitability and blast radius.

Severity conventions:

| Severity | Meaning | Examples |
|---|---|---|
| CRITICAL | Exploitable or data-destroying now | `eval` on untrusted input; unquoted var in `rm -rf`; secrets in argv/`set -x`; curl\|bash of unpinned URL in prod |
| HIGH | Will corrupt/fail on realistic input or failure | unquoted expansions in destructive paths; missing `set -e`/error checks around critical steps; predictable temp files; non-atomic config writes; missing `exec` in entrypoint (signals lost) |
| MEDIUM | Latent bug or fragility | parsing `ls`; `which` instead of `command -v`; missing `pipefail`; no `--` separators; no timeouts on network calls; `echo` for variable data |
| LOW | Style/maintainability with safety implications | missing `local`; `[ ]` where `[[ ]]` intended; missing `readonly`; inconsistent error messages |

Finding format:

```
[SEVERITY] file:line — short title (SCxxxx if applicable)
  Evidence: the offending line(s), verbatim
  Impact: what input/condition triggers it and what breaks
  Fix: concrete replacement code
  Effort: trivial | small | medium | large
```

## Rules index

| File | Covers |
|---|---|
| [rules/01-safety-baseline.md](rules/01-safety-baseline.md) | Shebang discipline, `set -euo pipefail` and its real limitations, quoting & word-splitting bug catalog, **zsh-vs-bash deviations that bite pasted commands (joining, `pipestatus`, and `NOMATCH` — an unquoted glob in a flag value aborts the command and fakes a clean sweep)** |
| [rules/06-ad-hoc-commands.md](rules/06-ad-hoc-commands.md) | The commands nobody commits — a sweep, a probe, a one-liner from a checklist, a quick container copy: the zsh deviations that bite pasted commands (joining, `pipestatus`, `NOMATCH` faking a clean sweep), **what your searcher silently excludes** (`-r` vs symlinked dirs, ripgrep's gitignore defaults, a `grep` that is really a wrapper) and controlling a sweep in the same invocation, and **blast radius** — an ad-hoc command that fills a disk or corrupts the runtime it was inspecting |
| [rules/05-constructs-and-cleanup.md](rules/05-constructs-and-cleanup.md) | The constructs a script is assembled from, once the expansion itself is right: arrays for command building (SC2089/2090), IFS scoping, `trap`-based cleanup and `mktemp` (predictable temp paths are a race, not a style point), `[[ ]]`/printf/`local`/`readonly`, and globbing pitfalls including never parsing `ls` |
| [rules/02-robustness-correctness.md](rules/02-robustness-correctness.md) | Argument parsing (getopts/while-case, --help/--version), input validation, POSIX vs bash portability, stderr/exit-code discipline, PIPESTATUS, command -v, network timeouts & retries, flock & background jobs, idempotency & atomic writes, safe filename handling |
| [rules/03-security.md](rules/03-security.md) | eval/injection, secrets discipline (argv/env/set -x), PATH hygiene, sudo discipline, curl\|bash both directions, temp-file races, umask, ShellCheck+shfmt in CI |
| [rules/04-ci-and-operational.md](rules/04-ci-and-operational.md) | GitHub Actions shell pitfalls (`${{ }}` injection, multiline run, quoting in YAML), container entrypoints (exec, PID 1, privilege drop), Makefile shell gotchas, long-running script logging |

## Top 10 non-negotiables

1. `#!/usr/bin/env bash` + `set -euo pipefail` on every bash script — and explicit error
   handling where `-e` is known not to fire (conditions, `&&`/`||`, command substitution
   in assignments-with-modifiers, process substitution).
2. Quote **every** expansion: `"$var"`, `"$@"`, `"${arr[@]}"`, `"$(cmd)"`. SC2086 is a
   bug, not style.
3. Build argument lists with arrays; pass them as `"${args[@]}"`. Never accumulate a
   command in a string and `eval`/word-split it.
4. `trap cleanup EXIT` with an idempotent cleanup function; temp paths only via `mktemp`.
5. Never `eval`, `bash -c`, or `sh -c` with interpolated untrusted data; use `--` before
   positional file/user arguments to every command that supports it.
6. No secrets in argv, in environment dumps, or under `set -x`; `set +x` around sensitive
   sections; read secrets from files or fds.
7. Errors to stderr with context (`script: failed to X: $detail`); meaningful exit codes;
   never `exit 0` on failure paths.
8. Network calls get `--fail`, `--max-time`/timeouts, and bounded retries — never bare
   `curl url | ...`.
9. Handle arbitrary filenames: `find -print0 | xargs -0` / `-exec ... +`, `while IFS= read -r`,
   never iterate `$(ls)` or unquoted globs from variables.
10. ShellCheck (clean, or annotated suppressions) + shfmt enforced in CI; a shell script
    without CI linting is unreviewed code.

## Cross-references

- CI pipeline hardening, `${{ }}` injection, action pinning → `sota-devsecops`
- Secret storage/rotation → `sota-secrets-management`
- Container image/runtime hardening → `sota-sandboxing`, `sota-cloud-infrastructure`
- When the "don't use shell" rule fires → `sota-python` / `sota-golang`

