Claude Skill

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 · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download iliaal-ai-skills-skills_linux-bash-scripting-7b0520a.zip · 11 KB
Part of iliaal/ai-skills — 26 skills

Install

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

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole iliaal/ai-skills 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 (ai-skills)
  • 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 7.5 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.
      - A script that shells out to a coding-agent or automation CLI with a prompt or task file whose content the script's author does not fully control (a ticket body, a generated plan, a file from another repo) must default to that CLI's most restrictive approval and execution mode. Never hardcode a full-auto or bypass flag. The task content is an indirect prompt-injection surface, so the execution mode is the last boundary the script can still enforce. Where an operator override is supported, validate the value against an explicit allowlist and reject anything unrecognized instead of passing it through:
        ```bash
        mode=${AGENT_MODE:-read-only}
        case $mode in
            read-only|ask) ;;
            *) die "Unsupported AGENT_MODE: $mode" ;;
        esac
        agent_cli --approval-mode "$mode" --task-file "$task"
        ```
        The allowlist deliberately omits the CLI's unattended mode; adding it is a reviewed change to the script, not an environment variable.
      
      ## 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: 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 (2 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