Claude Skill

ia-linux-bash-scripting

Defensive Bash scripting for Linux: safe foundations, argument parsing, production patterns, ShellCheck compliance. Use when writing bash scripts, shell scripts, cron jobs, or CLI tools in bash.

LLM Mart · 0 points · 10 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download iliaal-whetstone-plugins_whetstone_skills_ia-linux-bash-scripting-0a409ba.zip · 11 KB
Part of iliaal/whetstone — 62 skills

Install

skills CLI npx skills add https://github.com/iliaal/whetstone/tree/master/plugins/whetstone/skills/ia-linux-bash-scripting
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install iliaal-whetstone@llmmart
Git git clone https://github.com/iliaal/whetstone.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole iliaal/whetstone collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Linux Bash Scripting

Produce bash scripts that pass shellcheck --enable=all and shfmt -d with zero warnings.

Target: GNU Bash 4.4+ on Linux. No macOS/BSD workarounds, no Windows paths, no POSIX-only restrictions.

Script Foundation

#!/usr/bin/env bash
set -Eeuo pipefail
shopt -s inherit_errexit

readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"

trap 'printf "Error at %s:%d\n" "${BASH_SOURCE[0]}" "$LINENO" >&2' ERR
trap 'rm -rf -- "${_tmpdir:-}"' EXIT
  • -E propagates ERR traps into functions
  • inherit_errexit propagates errexit into $() command substitutions
  • Resolve the script's own data files against SCRIPT_DIR, never the caller's cwd or git rev-parse --show-toplevel. A shared linter invoked from another project's git hook, a cron job, or a wrapper runs with someone else's cwd, so a caller-relative rules path resolves to a file that does not exist: the rule set loads empty, zero violations are found, exit 0. It is a silent no-op, not an error, and running it from inside its own repo passes for the wrong reason. Exercise it once from a scratch directory that is not the script's own tree
  • Always create temp dirs under the EXIT trap: _tmpdir=$(mktemp -d)
  • Wrap body in main() { ... } with source guard: [[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@" -- enables sourcing for testing

Core rules

  • Quote expansions, use arrays for commands, and never evaluate external data as shell code.
  • Validate numeric syntax, sign, and application bounds before arithmetic. Convert unsigned digits with 10# before applying the sign; 10#-08 is invalid.
  • Keep secrets out of process arguments and tracing. Feed them through stdin and use a JSON-aware encoder.
  • Check exact exit statuses where “absent” differs from “failed to inspect.” Separate local declarations from command substitutions.
  • Use NUL-delimited file iteration, validate required flag values, and reject conflicting output/target combinations.
  • For atomic replacement, stage beside the destination; for multi-file activation, switch a single staged release reference.
  • Preserve unrelated files and report the actual signal or command status after cleanup.
  • Do not assume Bash options work in sh, GNU utility modes behave like syscall modes, or a configured fallback path is usable.

Task-specific references

Read the relevant reference before implementing the matching behavior:

  • For command execution, external input, numeric conversion, argument parsing, iteration, or subprocess status handling: input-and-process-safety.md.
  • For file activation, secrets, locking, retries, cleanup, permissions, logging, or restartable automation: production-patterns.md.

Performance

  • Parameter expansion over externals: ${path%/*} not dirname, ${path##*/} not basename, ${var//old/new} not sed
  • (( )) over expr; [[ =~ ]] over echo | grep
  • Cache results: val=$(cmd) once, reuse $val
  • xargs -0 -P "$(nproc)" for parallel work
  • declare -A map for lookups instead of repeated grep

Bash 4.4+ / 5.x

  • ${var@Q} shell-quoted, ${var@U} uppercase, ${var@L} lowercase
  • declare -n ref=varname nameref for indirect access
  • wait -n wait for any background job
  • $EPOCHSECONDS, $EPOCHREALTIME -- timestamps without forking date

Linux-Specific

  • GNU coreutils differ from macOS: sed -i (no '' suffix), grep -P (PCRE support), readlink -f (canonical path)
  • timeout 30s cmd to prevent automation hangs

ShellCheck

Run shellcheck --enable=all script.sh. Key rules:

  • SC2155: Separate declaration from assignment
  • SC2086: Double-quote variables
  • SC2046: Quote command substitutions
  • SC2164: cd dir || exit
  • SC2327/SC2328: Use ${BASH_REMATCH[n]} not $n for regex captures

Pre-commit: shellcheck *.sh && shfmt -i 2 -ci -d *.sh

Verify

Run shellcheck --enable=all and shfmt -d with zero warnings before declaring done. Test edge cases: empty input, missing files, spaces in paths.

If shellcheck or shfmt is not installed (command -v shellcheck fails), the check was skipped, not passed: report "static analysis not run: shellcheck unavailable" and fall back to bash -n for syntax only. A skipped linter is the same silent no-op as the empty rules file above.

Files (whetstone)
  • references
    • input-and-process-safety.md 6.3 KB
      # Input and process safety
      
      ## Core Rules
      
      - Quote every expansion: `"$var"`, `"$(cmd)"`, `"${array[@]}"`
      - `local` for function variables, `local -r` for function constants, `readonly` for script constants
      - `printf '%s\n'` over `echo` -- predictable behavior, no flag interpretation
      - `[[ ]]` for conditionals; `(( ))` for arithmetic; `$()` over backticks
      - End options with `--`: `rm -rf -- "$path"`, `grep -- "$pattern" "$file"`
      - Require env vars: `: "${VAR:?must be set}"`
      - Never `eval` user input; build commands as arrays: `cmd=("grep" "--" "$pat" "$f"); "${cmd[@]}"`
      - Keep untrusted/derived bytes off the command line: never build a heredoc body or an `sh -c` string from external data. An unquoted `<<EOF` command-substitutes `$(...)`/backticks in the content, and even a quoted `<<'EOF'` breaks if a content line equals the delimiter (the heredoc ends early and the rest runs as shell). Write the data to a file with a non-shell writer and have the consumer read the file
      - Allowlisting a command? Match the whole command against an anchored pattern (`^…$`), never inspect individual arguments — shell operators (`;`, `&&`, `|`, `#`, newline) smuggle a second command past a per-argument check (`rm -rf node_modules; rm -rf /`). Unrecognized syntax must fail closed to deny/ask
      - Validating a path component before it reaches a destructive command? Anchor it against an allowlist (`[[ "$name" =~ ^[a-z0-9][a-z0-9._-]*$ ]]`) before `rm -rf -- "$base/$name"` -- a prefix/`startswith` check on the joined path is defeated by `../` (`$base/../x` still starts with `$base`) and by a sibling directory sharing the prefix (`/srv/app` matches `/srv/app2`). When a full path must be accepted, `realpath -e` it and compare against the resolved base plus a trailing slash
      - Validate external numeric text before arithmetic: array subscripts can execute commands, and leading zeros select octal. Require `[[ "$v" =~ ^-?[0-9]+$ ]]`, separate an optional minus from the digits, reject magnitudes outside the application's range before arithmetic conversion, convert the unsigned digits with `10#`, then apply the sign. `10#-08` is invalid. See Bounded signed decimal conversion below.
      - Separate `local` from assignment to preserve exit codes: `local val; val=$(cmd)`
      - Debug tracing: `PS4='+${BASH_SOURCE[0]}:${LINENO}: '` with `bash -x` -- shows file:line per command
      - Named exit codes: `readonly EX_USAGE=64 EX_CONFIG=78` -- no magic numbers in `exit`
      - Pipeline diagnostics: `"${PIPESTATUS[@]}"` shows exit code of each pipe stage, not just last failure
      - Branch on a probe's exact exit status, not on nonzero-versus-zero. A tool that exits 2 for "ran, found nothing" and 128 for "could not run" collapses into a single negative under `if ! cmd`, and stderr is often empty for both. Treating every silent nonzero as "absent" converts a network, permission, or spawn failure into a confident false diagnosis
      - `A || B` is a fallback only when `A` **fails** on the case `B` exists for. When `A` succeeds while doing the wrong thing -- resolving a different tool, default, or directory -- `B` is dead code and the wrong behavior is silent. Same trap in `${VAR:-default}` on a path two processes must agree on: whoever lacks `VAR` gets a different location, the two silently stop sharing state, and neither errors. Pick one resolution and fail loudly when it is unavailable. A fallback chain must also test *usability*, not presence: `${XDG_RUNTIME_DIR:-/tmp}` falls through only when the variable is unset, so a variable pointing at an unwritable directory takes `mkdir` to `EACCES` and aborts on the step the chain made optional. Treat a permission or existence failure on a configured location the same as an unconfigured one, and log which candidate was chosen
      - A `;` list exits with its *last* command's status, so appending a status echo guarantees success: `./run.sh > log 2>&1; echo "EXIT=$?"` exits 0 no matter what `run.sh` did. Capture and re-raise: `rc=$?; printf 'EXIT=%d\n' "$rc"; exit "$rc"`
      - A wait loop that greps for the process it waits on matches itself: `pgrep -f` tests the full argv and the pattern sits in the waiter's own command line, so `until ! pgrep -f build_step; do sleep 10; done` never exits. A pipeline feeding `ps` straight into a matcher includes the matcher's own process the same way, and an empty substitution collapses `/proc/$(pgrep -f cmd | head -1)` to `/proc/`, which always exists. Match the exact process name (`pgrep -x`), drop your own PID (`pgrep -f "$pat" | grep -vx "$$"`), take the snapshot in one command and filter the saved output in the next, and prefer waiting on the process directly (`wait`, `flock`) over polling for it
      
      
      ## Safe Iteration
      
      ```bash
      # NUL-delimited file processing
      while IFS= read -r -d '' f; do
          process "$f"
      done < <(find /path -type f -name '*.log' -print0)
      
      # Array from command output
      readarray -t lines < <(command)
      readarray -d '' files < <(find . -print0)
      
      # Glob with no-match guard
      for f in *.txt; do [[ -e "$f" ]] || continue; process "$f"; done
      ```
      
      ## Argument Parsing
      
      ```bash
      verbose=false; output=""
      while [[ $# -gt 0 ]]; do
          case "$1" in
              -v|--verbose) verbose=true; shift ;;
              -o|--output)  output="$2"; shift 2 ;;
              -h|--help)    usage; exit 0 ;;
              --)           shift; break ;;
              -*)           printf 'Unknown: %s\n' "$1" >&2; exit 1 ;;
              *)            break ;;
          esac
      done
      ```
      
      A single-destination override flag (`--out FILE`) combined with more than one positional target clobbers silently -- last write wins, no error, no diagnostic. Detect the combination (`(( ${#targets[@]} > 1 )) && [[ -n "$output" ]]`) and exit `EX_USAGE` instead of letting the last target overwrite every prior one.
      
      
      ## Bounded signed decimal conversion
      
      Accept integers from -999999999 to 999999999 in this example, including zero padding. Choose and check the application's actual range before arithmetic; Bash integers do not detect overflow.
      
      ```bash
      parse_decimal() {
          local raw=${1-} digits sign=1
          [[ "$raw" =~ ^-?[0-9]+$ ]] || return 64
          digits=${raw#-}
          [[ "$raw" != -* ]] || sign=-1
          while [[ ${#digits} -gt 1 && "$digits" == 0* ]]; do
              digits=${digits#0}
          done
          [[ ${#digits} -le 9 ]] || return 64
          printf '%d\n' "$((sign * 10#$digits))"
      }
      ```
      
      Check `08 → 8`, `-08 → -8`, `-0 → 0`, and reject empty text, expression syntax, and magnitudes beyond the bound.
      
    • production-patterns.md 6.6 KB
      # Production patterns
      
      ## Production Patterns
      
      **Dependency check:**
      ```bash
      require() { command -v "$1" &>/dev/null || { printf 'Missing: %s\n' "$1" >&2; exit 1; }; }
      require jq; require curl
      ```
      
      **Dry-run wrapper:**
      ```bash
      run() { if [[ "${DRY_RUN:-}" == "1" ]]; then printf '[dry] %s\n' "$*" >&2; else "$@"; fi; }
      run cp "$src" "$dst"
      ```
      
      **Atomic file write** -- run the producer into a temp file, then rename only after it succeeds:
      ```bash
      atomic_write() {
          local target=$1 tmp
          shift
          tmp=$(mktemp -- "${target}.tmp.XXXXXXXX") || return
          if "$@" >"$tmp" && mv -fT -- "$tmp" "$target"; then
              return 0
          else
              local rc=$?
              rm -f -- "$tmp"
              return "$rc"
          fi
      }
      atomic_write /etc/app/config.yml generate_config
      ```
      
      The producer must return nonzero on generation failure; explicitly propagate failures inside shell functions because Bash suppresses `errexit` in this conditional context. Pipeline producers must enable `pipefail`. Do not pipe into this helper: an upstream failure cannot prevent a rename that already happened.
      
      **Atomic multi-file activation** -- N individually atomic copies are not an atomic interface: a failure after replacing the second of three leaves the old entry point running against a mixed set. Stage the release into a fresh uniquely-named directory, then swap one relative `current` symlink (`ln -sfn` onto a temp name, then `mv -T` it into place). A component that cannot join the swap -- a separately installed helper that an already-running caller invokes -- is installed *first*, so an interrupted run lands on old-caller/new-helper, and the helper's interface stays backward compatible. The failure fixture seeds a complete prior release, fails after one new component is staged, and asserts every prior component is still active.
      
      **Retry with backoff:**
      ```bash
      retry() { local n=0 max=5 delay=1; until "$@"; do ((++n>=max)) && return 1; sleep $delay; ((delay*=2)); done; }
      retry curl -fsSL "$url"
      ```
      
      **Script locking** -- prevent concurrent runs:
      ```bash
      exec 9>/var/lock/"${0##*/}".lock
      flock -n 9 || { printf 'Already running\n' >&2; exit 1; }
      ```
      
      **Idempotent operations** -- safe to rerun:
      ```bash
      ensure_dir()  { [[ -d "$1" ]] || mkdir -p -- "$1"; }
      ensure_link() { [[ -L "$2" ]] || ln -s -- "$1" "$2"; }
      ```
      
      A linear script with irreversible steps (commit, push, tag, publish) must be re-runnable from any failure point, not just idempotent per primitive: make each step check-and-skip (`release_exists "$tag" || create_release "$tag"`) so a failure at step 4 is repaired by one re-invocation instead of a hand-reconstruction of steps 4-6.
      
      **Input validation:** `[[ "$1" =~ ^[1-9][0-9]*$ ]] || die "Invalid: $1"` -- validate at script boundaries with `[[ =~ ]]`. The leading `[1-9]` also excludes zero-padded input, which arithmetic would read as octal; widening this to `^[0-9]+$` to admit `0` reintroduces that trap unless the value goes through `10#`
      
      - `umask 077` for scripts creating sensitive files
      - Distinguish syscall modes from utility options: umask masks a `mkdir(2)` mode, but GNU `mkdir -m 755` explicitly sets the resulting directory to `0755` even under `umask 077`. For a mode-preservation test, set the fixture's mode explicitly and assert the observed result; do not loosen the service's umask.
      - Staging a file across users through a world-writable directory fails on the rename, not the read: `/tmp`'s sticky bit lets only the file's owner rename or unlink it, so a second user's `mv /tmp/f "$dest"` fails with `Operation not permitted` while `cp` succeeds. Copy as the destination user (`sudo -u <dest> cp -- /tmp/f "$target"`), then remove the staging copy as its creator
      - Moving a secret out of argv into a temp file closes the `ps` / `/proc/<pid>/cmdline` exposure and nothing else. Bash stores a multi-line command as **one** history entry, heredoc body included, and the single-line form `printf %s '<value>' >"$tmp"` puts the value on the command line too. Take it from stdin and let a JSON-aware writer escape it:
        ```bash
        umask 077; tmp=$(mktemp); trap 'rm -f -- "$tmp"' EXIT
        read -rs SECRET                                   # stdin: never a command line, never in history
        printf '%s' "$SECRET" | jq -Rs '{Password:.}' >"$tmp"
        ```
        Bash's builtin `printf` sends the value through stdin, avoiding `jq --arg`'s process-argument exposure; `jq -Rs` escapes JSON characters. Use `mktemp` rather than a predictable path that an attacker can replace with a symlink.
      - Generate secret/token files with no trailing newline. `cmd >"$f"` keeps the `\n`, `$(cat "$f")` strips it, and CLI arguments of the `file://$f` shape transmit it verbatim -- so one generated value installed into two consumers differs by one byte while both sides *display* the same characters and every constant-time comparison on the far side just returns false. Fix at the generator (`printf %s "$(cmd)" >"$f"`), never per reader, and verify with `wc -c < "$f"`
      - Signal cleanup: use `trap 'cleanup; exit 130' INT` and `trap 'cleanup; exit 143' TERM` to report the conventional signal-specific exit status.
      
      ## Logging
      
      ```bash
      log() { printf '[%s] [%s] %s\n' "$(date -Iseconds)" "$1" "${*:2}" >&2; }
      info()  { log INFO "$@"; }
      warn()  { log WARN "$@"; }
      error() { log ERROR "$@"; }
      die()   { error "$@"; exit 1; }
      ```
      
      ## Anti-Patterns
      
      | Bad | Fix |
      |-----|-----|
      | `for f in $(ls)` | `for f in *; do` or `find -print0 \| while read` |
      | `local x=$(cmd)` | `local x; x=$(cmd)` -- preserves exit code |
      | `x=$(cmd)` then an `[[ -z $x ]]` fallback check | `x=$(cmd) \|\| true` -- under `set -e` a failed `$()` in a bare assignment aborts the script there, so the fallback never runs (opposite of the `local` case: `local` masks the failure, a bare assignment propagates it) |
      | `x=$(cmd 2>/dev/null \|\| echo MISSING)` | Capture and test separately -- a tool that prints to stdout *and* exits nonzero (some echo their unresolved argument before failing) contributes both strings, so `x` becomes `<junk>` + `MISSING` and every comparison built on it reports a spurious difference. The `2>/dev/null` that quiets the loop is also what hides the error line |
      | `echo "$data"` | `printf '%s\n' "$data"` |
      | `cat file \| grep` | `grep pat file` |
      | `kill -9 $pid` first | `kill "$pid"` first, `-9` as last resort |
      | `cd dir; cmd` | `cd dir || exit 1` or subshell `(cd dir && cmd)` |
      | A multi-command shell block embedded in YAML or a `RUN` line | Select Bash explicitly before using `set -Eeuo pipefail`; these options are not portable to `sh`. Match the production interpreter and flags in tests. Capture failures before a trailing successful command can hide them, and keep intentional fallbacks explicit. |
      
  • SKILL.md 4.6 KB
    ---
    name: ia-linux-bash-scripting
    class: language
    description: >-
      Defensive Bash scripting for Linux: safe foundations, argument parsing,
      production patterns, ShellCheck compliance. Use when writing bash scripts,
      shell scripts, cron jobs, or CLI tools in bash.
    paths: "**/*.sh,**/*.bash"
    ---
    
    # Linux Bash Scripting
    
    Produce bash scripts that pass `shellcheck --enable=all` and `shfmt -d` with zero warnings.
    
    Target: GNU Bash 4.4+ on Linux. No macOS/BSD workarounds, no Windows paths, no POSIX-only restrictions.
    
    ## Script Foundation
    
    ```bash
    #!/usr/bin/env bash
    set -Eeuo pipefail
    shopt -s inherit_errexit
    
    readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
    
    trap 'printf "Error at %s:%d\n" "${BASH_SOURCE[0]}" "$LINENO" >&2' ERR
    trap 'rm -rf -- "${_tmpdir:-}"' EXIT
    ```
    
    - `-E` propagates ERR traps into functions
    - `inherit_errexit` propagates errexit into `$()`  command substitutions
    - Resolve the script's own data files against `SCRIPT_DIR`, never the caller's cwd or `git rev-parse --show-toplevel`. A shared linter invoked from another project's git hook, a cron job, or a wrapper runs with someone else's cwd, so a caller-relative rules path resolves to a file that does not exist: the rule set loads empty, zero violations are found, exit 0. It is a silent no-op, not an error, and running it from inside its own repo passes for the wrong reason. Exercise it once from a scratch directory that is not the script's own tree
    - Always create temp dirs under the EXIT trap: `_tmpdir=$(mktemp -d)`
    - Wrap body in `main() { ... }` with source guard: `[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"` -- enables sourcing for testing
    
    ## Core rules
    
    - Quote expansions, use arrays for commands, and never evaluate external data as shell code.
    - Validate numeric syntax, sign, and application bounds before arithmetic. Convert unsigned digits with `10#` before applying the sign; `10#-08` is invalid.
    - Keep secrets out of process arguments and tracing. Feed them through stdin and use a JSON-aware encoder.
    - Check exact exit statuses where “absent” differs from “failed to inspect.” Separate `local` declarations from command substitutions.
    - Use NUL-delimited file iteration, validate required flag values, and reject conflicting output/target combinations.
    - For atomic replacement, stage beside the destination; for multi-file activation, switch a single staged release reference.
    - Preserve unrelated files and report the actual signal or command status after cleanup.
    - Do not assume Bash options work in `sh`, GNU utility modes behave like syscall modes, or a configured fallback path is usable.
    
    ## Task-specific references
    
    Read the relevant reference before implementing the matching behavior:
    
    - For command execution, external input, numeric conversion, argument parsing, iteration, or subprocess status handling: [input-and-process-safety.md](./references/input-and-process-safety.md).
    - For file activation, secrets, locking, retries, cleanup, permissions, logging, or restartable automation: [production-patterns.md](./references/production-patterns.md).
    
    ## Performance
    
    - Parameter expansion over externals: `${path%/*}` not `dirname`, `${path##*/}` not `basename`, `${var//old/new}` not `sed`
    - `(( ))` over `expr`; `[[ =~ ]]` over `echo | grep`
    - Cache results: `val=$(cmd)` once, reuse `$val`
    - `xargs -0 -P "$(nproc)"` for parallel work
    - `declare -A map` for lookups instead of repeated grep
    
    ## Bash 4.4+ / 5.x
    
    - `${var@Q}` shell-quoted, `${var@U}` uppercase, `${var@L}` lowercase
    - `declare -n ref=varname` nameref for indirect access
    - `wait -n` wait for any background job
    - `$EPOCHSECONDS`, `$EPOCHREALTIME` -- timestamps without forking `date`
    
    ## Linux-Specific
    
    - GNU coreutils differ from macOS: `sed -i` (no `''` suffix), `grep -P` (PCRE support), `readlink -f` (canonical path)
    - `timeout 30s cmd` to prevent automation hangs
    
    ## ShellCheck
    
    Run `shellcheck --enable=all script.sh`. Key rules:
    - **SC2155**: Separate declaration from assignment
    - **SC2086**: Double-quote variables
    - **SC2046**: Quote command substitutions
    - **SC2164**: `cd dir || exit`
    - **SC2327/SC2328**: Use `${BASH_REMATCH[n]}` not `$n` for regex captures
    
    Pre-commit: `shellcheck *.sh && shfmt -i 2 -ci -d *.sh`
    
    ## Verify
    
    Run `shellcheck --enable=all` and `shfmt -d` with zero warnings before declaring done. Test edge cases: empty input, missing files, spaces in paths.
    
    If `shellcheck` or `shfmt` is not installed (`command -v shellcheck` fails), the check was skipped, not passed: report "static analysis not run: shellcheck unavailable" and fall back to `bash -n` for syntax only. A skipped linter is the same silent no-op as the empty rules file above.
    
  • SPEC.md 4.5 KB
    # ia-linux-bash-scripting Specification
    
    ## Intent
    
    `ia-linux-bash-scripting` is a `language`-class skill (stack-specific patterns and idioms). Defensive Bash scripting for Linux: safe foundations, argument parsing, production patterns, ShellCheck compliance. Use when writing bash scripts, shell scripts, cron jobs, or CLI tools in bash.
    
    ## Scope
    
    In scope:
    - Behaviors described in `SKILL.md` and routed via the should_trigger phrasings in `distillery/tests/fixtures/triggers/ia-linux-bash-scripting.jsonl`.
    - Updates to runtime behavior, structure, trigger precision, references, and validation.
    
    Out of scope:
    - Acting as the runtime instructions themselves (those live in `SKILL.md`).
    - Trigger phrasings already covered by adjacent `ia-*` skills (`validate-plugin` flags >70% description overlap as DUPLICATE_TRIGGER).
    - <!-- to fill in: domain-specific exclusions when the skill drifts -->
    
    ## Trigger Context
    
    - Class: `language`
    - Hook regex: `plugins/whetstone/hooks/skill-patterns.sh` -> `SKILL_PATTERNS[ia-linux-bash-scripting]`
    - Common requests (from fixture should_trigger):
      - "write a bash script to automate the database backup"
      - "create a deployment script for the production servers"
      - "write a bash script to rotate the nightly backups"
    - Should not trigger for (from fixture should_not_trigger):
      - "build a React form with validation"
      - "add a new Laravel middleware for API throttling"
      - "write a Python CLI for log parsing"
    
    ## Source And Evidence Model
    
    Authoritative sources:
    
    - `SKILL.md` -- runtime instructions and reference routing.
    - `references/*.md` -- bundled supplementary content (0 file(s)).
    - `distillery/tests/fixtures/triggers/ia-linux-bash-scripting.jsonl` -- positive and negative trigger phrasings under regression test.
    - `plugins/whetstone/hooks/skill-patterns.sh` -- regex pattern that fires this skill.
    - `distillery/.eval-data/ia-linux-bash-scripting/` -- harvested session examples (when present).
    
    Data that must not be stored in this skill or its references:
    
    - Secrets, credentials, tokens.
    - Machine-specific filesystem paths (`/home/...`, `/Users/...`, `~/ai/...`). The validator (`MACHINE_PATH_LEAK`) flags these as HIGH.
    - Private URLs, customer data, or unredacted personal information.
    
    ### Coverage matrix
    
    | Dimension | Status | Evidence |
    |---|---|---|
    | Trigger fixtures | complete | distillery/tests/fixtures/triggers/ia-linux-bash-scripting.jsonl (>=5 should_trigger, >=5 should_not_trigger) |
    | Hook regex pattern | complete | plugins/whetstone/hooks/skill-patterns.sh (`SKILL_PATTERNS[ia-linux-bash-scripting]`) |
    | Reference architecture | n/a | no references; SKILL.md is self-contained |
    | Real-usage signal | <!-- populated by harvest-sessions when sessions exist --> | distillery/.eval-data/ia-linux-bash-scripting/ (created by harvest-sessions) |
    
    ## Evaluation
    
    Lightweight (run on every change):
    
    ```bash
    python3 distillery/scripts/distiller.py validate-plugin --component ia-linux-bash-scripting
    python3 distillery/scripts/distiller.py test-triggers --skill ia-linux-bash-scripting
    ```
    
    Deeper (when behavior risk warrants):
    
    ```bash
    python3 distillery/scripts/distiller.py dspy-eval ia-linux-bash-scripting
    python3 distillery/scripts/distiller.py diagnose-negatives ia-linux-bash-scripting
    ```
    
    Acceptance gates:
    - `validate-plugin --component ia-linux-bash-scripting` returns 0 HIGH findings.
    - `test-triggers --skill ia-linux-bash-scripting` returns F1 = 1.0 with floors of 5 should_trigger and 5 should_not_trigger.
    - For dspy-eval, the composite score does not regress against the most recent saved baseline (see `distillery/.eval-data/ia-linux-bash-scripting/history.json`).
    
    ## Known Limitations
    
    <!-- to fill in over time as drift surfaces. Default rule: any time diagnose-negatives
         surfaces a recurring failure pattern, document it here so future maintainers
         understand the trade-off the current implementation accepts. -->
    
    ## Maintenance Notes
    
    - Update `SKILL.md` when the runtime workflow, branch conditions, or output contract changes.
    - Update this `SPEC.md` when intent, scope, evidence model, evaluation gates, or maintenance expectations change.
    - Update the trigger fixture when adding new positive phrasings, removing stale ones, or expanding scope (the 5/5 floor is a hard validator gate).
    - Update the hook regex in `skill-patterns.sh` whenever fixture positives expose a missed phrasing; verify F1 = 1.0 with `eval-triggers` before committing.
    - Run the full release pipeline via `/release` -- never bump versions or update CHANGELOG.md from a per-skill edit.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related