agentleFS
jaskirat1616jaskirat1616/grok-skills/skills/bash-script-hardening/SKILL.md

bash-script-hardening

Skill22 starsChanged 2 months agoMITView on GitHub

Writes robust, safe, shellcheck-clean Bash scripts using strict mode, defensive quoting, error traps, safe temp files, and signal handling. Use this skill when authoring or reviewing shell/Bash scripts, when asked to "harden a bash script", "add strict mode", "fix shellcheck warnings", "make this script safe", debug silent failures, unquoted-variable bugs, word-splitting/globbing issues, or to add cleanup traps and error handling.

  • Deletes or force-pushes

We split this skill into the rules it gives an agent

11 rules. About 0 are common, 4 are distinctive, and 7 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 7

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this skill to author a new script from a hardened template, retrofit an existing script, or review a script for safety defects.

    line 15
  2. Very specific:

    Set the shebang and strict mode: Use #!/usr/bin/env bash and the canonical strict-mode preamble (see below).

    line 19
  3. Distinctive:

    Quote everything: Every variable expansion and command substitution gets double quotes unless you have a deliberate, commented reason not …

    line 21
  4. Distinctive:

    Use arrays for lists and command arguments: Never build commands by concatenating strings.

    line 22
  5. Very specific:

    Use "${arr[@]}" (quoted, @) to preserve elements.

    line 60
  6. Very specific:

    Prefer [[ ... ]] over [ ...

    line 61
  7. Very specific:

    Use printf '%s\n' "$x" instead of echo "$x" for arbitrary data (echo mangles -n, backslashes).

    line 62
  8. Very specific:

    Never use /tmp/myscript.$$ — predictable names are a symlink-attack vector and collide.

    line 106
  9. Very specific:

    Separate config from logic: Read tunables from env with documented defaults: : "${TIMEOUT:=30}".

    line 143
  10. Distinctive:

    Log to stderr, data to stdout: Keep stdout pure so the script composes in pipelines.

    line 144
  11. Very specific:

    Pin behavior, not the environment: Use full option names in comments.

    line 146

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Bash Script Hardening

Overview

Most Bash bugs are silent: an unset variable expands to nothing, a failed command is ignored, an unquoted path word-splits, and the script marches on corrupting state. This skill makes Bash scripts fail loudly, fail early, and clean up after themselves.

Keywords: bash, shell, strict mode, set -euo pipefail, IFS, quoting, word splitting, globbing, ShellCheck, trap, ERR, EXIT, cleanup, mktemp, signal handling, defensive scripting, POSIX, exit codes, here-doc, arrays.

Use this skill to author a new script from a hardened template, retrofit an existing script, or review a script for safety defects.

Workflow

  1. Set the shebang and strict mode. Use #!/usr/bin/env bash and the canonical strict-mode preamble (see below). Never rely on /bin/sh if Bash features are used.
  2. Lock down word splitting. Set IFS=$'\n\t' so unquoted expansions don't split on spaces.
  3. Quote everything. Every variable expansion and command substitution gets double quotes unless you have a deliberate, commented reason not to.
  4. Use arrays for lists and command arguments. Never build commands by concatenating strings.
  5. Add a cleanup trap. Register an EXIT trap that removes temp files and restores state, plus an ERR trap for diagnostics.
  6. Handle errors explicitly where strict mode is insufficient (conditionals, command substitution in older Bash, pipelines you intend to tolerate).
  7. Create temp files safely with mktemp, and reference them through the cleanup trap.
  8. Validate inputs and dependencies up front: required args, required commands (command -v), required env vars.
  9. Run ShellCheck and resolve every finding; suppress only with a justified # shellcheck disable=SCxxxx comment.
  10. Test failure paths, not just the happy path.

Reference files:

  • references/strict-mode.md — deep dive on set -euo pipefail, IFS, and the gotchas/escape hatches.
  • references/shellcheck-codes.md — the most common ShellCheck codes, what they mean, and the correct fix.
  • templates/hardened-script.sh — drop-in starting template with strict mode, traps, arg parsing, logging.
  • scripts/harden_check.sh — static auditor that flags missing strict mode, unquoted vars, and other red flags.
  • examples/before-after.md — a real fragile script transformed into a hardened one, line by line.

The canonical strict-mode preamble

#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'

What each flag buys you:

FlagEffectWhy it matters
-e (errexit)Exit on any command returning non-zeroStops a broken script from continuing on corrupt state
-u (nounset)Error on use of unset variablesCatches typos like "$fle" and missing args
-o pipefailA pipeline fails if any stage failscmd | grep x no longer hides cmd crashing
-E (errtrace)ERR trap is inherited by functions/subshellsYour error trap actually fires inside functions
IFS=$'\n\t'Split only on newline/tab, not spacesFilenames with spaces stop silently breaking loops

-e has sharp edges. Read references/strict-mode.md for where it does NOT trigger (e.g. inside if, &&, ||, the last command of a function used as a condition) and the correct patterns to keep error checking explicit there.

Quoting and word-splitting rules

  • Always quote expansions: "$var", "${arr[@]}", "$(cmd)".
  • Use "${arr[@]}" (quoted, @) to preserve elements; never ${arr[*]} for argument lists.
  • Prefer [[ ... ]] over [ ... ] in Bash — it does not word-split or glob inside.
  • Use printf '%s\n' "$x" instead of echo "$x" for arbitrary data (echo mangles -n, backslashes).
  • Loop over files with globs or find -print0 | while IFS= read -r -d '', never for f in $(ls).
# WRONG — word-splits and globs
for f in $(ls *.txt); do rm $f; done

# RIGHT
shopt -s nullglob
for f in ./*.txt; do
  rm -- "$f"
done

Traps and cleanup

Register cleanup once, early, and make it idempotent:

cleanup() {
  local rc=$?
  # remove temp artifacts; restore anything you changed
  [[ -n "${tmpdir:-}" && -d "$tmpdir" ]] && rm -rf -- "$tmpdir"
  return $rc
}
trap cleanup EXIT

on_err() {
  local rc=$? line=$1
  printf 'ERROR: line %s exited with status %s\n' "$line" "$rc" >&2
}
trap 'on_err "$LINENO"' ERR

The EXIT trap runs on normal exit, error exit (with -e), and most signals — making it the single reliable place to clean up. See templates/hardened-script.sh for the full pattern including signal-specific traps.

Safe temp files

tmpdir="$(mktemp -d)"          # never hand-craft /tmp/$$ paths (race + predictable)
tmpfile="$(mktemp "$tmpdir/XXXXXX.json")"
# tmpdir is removed by the EXIT trap above

Never use /tmp/myscript.$$ — predictable names are a symlink-attack vector and collide.

Input and dependency validation

require_cmd() {
  command -v "$1" >/dev/null 2>&1 || { printf 'Missing dependency: %s\n' "$1" >&2; exit 127; }
}
require_cmd jq
require_cmd curl

: "${API_TOKEN:?API_TOKEN must be set}"   # nounset-style guard for required env

if (( $# < 1 )); then
  printf 'usage: %s <input-file>\n' "${0##*/}" >&2
  exit 2
fi

Hardening checklist

  • #!/usr/bin/env bash shebang
  • set -Eeuo pipefail and IFS=$'\n\t'
  • Every variable/command-substitution expansion is double-quoted
  • Lists and argv built as arrays, expanded with "${arr[@]}"
  • EXIT trap cleans temp files; ERR trap reports failures
  • Temp files via mktemp/mktemp -d, removed in trap
  • Required commands checked with command -v
  • Required args and env vars validated with clear usage + nonzero exit
  • -- used before user-supplied paths in rm, cp, etc.
  • No eval; no parsing ls; no unquoted $(...)
  • Meaningful, distinct exit codes (2 usage, 127 missing dep, etc.)
  • shellcheck passes clean (disables are justified inline)

Best Practices

  • Make scripts re-runnable (idempotent). Guard mutations so a re-run after partial failure is safe.
  • Separate config from logic. Read tunables from env with documented defaults: : "${TIMEOUT:=30}".
  • Log to stderr, data to stdout. Keep stdout pure so the script composes in pipelines.
  • Use functions with local variables; avoid global mutation. Declare local separately from command substitution to avoid masking exit codes (local x; x="$(cmd)").
  • Pin behavior, not the environment. Use full option names in comments; prefer printf over echo.
  • Fail with the right exit code. Reserve nonzero codes for distinct failure classes so callers can branch.
  • Run ShellCheck in CI and treat it as a gate. See references/shellcheck-codes.md.

Common Pitfalls

  • local x=$(cmd) swallows errors. The local builtin's own success masks cmd's exit status even under -e. Split into two lines.
  • -e does not fire inside if/&&/||/! or the condition of a loop. Don't assume strict mode covers everything; check return codes explicitly there.
  • cmd | while read ... runs the loop in a subshell — variables set inside are lost in older Bash. Use process substitution while read ...; do ...; done < <(cmd) instead.
  • set -e + a function whose last command "fails" as a test silently disables errexit for that function's body. Avoid using such functions as if conditions when you also rely on -e inside them.
  • Unquoted $@ vs "$@". Bare $@ re-splits arguments; always "$@".
  • read without -r mangles backslashes; always read -r, and set IFS= per-read to preserve leading/trailing whitespace.
  • Predictable temp paths (/tmp/$$) cause races and symlink attacks — always mktemp.
  • rm -rf "$dir/"* when $dir is empty/unset can target /. Quote, validate, and use --; consider ${dir:?} to abort on empty.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.