plugins/build/skills/check-shell/SKILL.md
Audits a general-purpose shell script against 14 curated lints grouped into Portability / Safety / Documentation, detects and merges findings from shellcheck / shfmt / checkbashisms when present, and surfaces a Missing Tools preamble with install hints when any are absent. Use when the user wants to "audit a shell script", "check this bash script", "review my shell script", "lint a script", or "is this script safe". Not for hook scripts — route to `/build:check-hook`.
npx skillsauth add bcbeidel/wos check-shellInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Inspect a general-purpose shell script for portability, safety, and
documentation problems. Run 14 curated lints plus output from
shellcheck / shfmt / checkbashisms when they are installed.
Read-only — reports findings but does not modify the script.
This skill is not for Claude Code hooks — /build:check-hook owns that
lifecycle. Route there when the script is wired to a hook event.
Workflow sequence: 1. Input → 2. Tool Detection → 3. Missing Tools Preamble → 4. Checks → 5. Report → 6. Handoff
If $ARGUMENTS is non-empty, read the script at that path. Otherwise
ask the user for a path. Read the script's shebang line and scan its
body to detect the target shell:
| Detected marker | Inferred target |
|-----------------|-----------------|
| #!/usr/bin/env bash or #!/bin/bash + [[ ]] / local / arrays | bash-* |
| declare -A, mapfile, wait -n, ${var^^} | bash-4+ |
| EPOCHSECONDS, nameref loops, BASH_ARGV0 | bash-5+ |
| #!/bin/sh or #!/usr/bin/env sh and no bash-only syntax | posix-sh |
| setuid bit present (stat -c %a or ls -l shows s) | record; suppresses the env bash recommendation |
If the target is ambiguous, state the ambiguity in the report header
and scope conservatively (treat as bash-3.2-portable unless a
bash-4+ feature is already in use).
Probe PATH for three tools. Record which are present and which are
absent.
command -v shellcheck >/dev/null 2>&1
command -v shfmt >/dev/null 2>&1
command -v checkbashisms >/dev/null 2>&1 # only relevant when target = posix-sh
Invocation and output parsing details live in external-tools.md.
When any relevant tool is absent from PATH, the report's first
section is a Missing Tools block. Format:
## Missing tools
- **shellcheck** not found on PATH.
Install: brew install shellcheck | apt install shellcheck | dnf install ShellCheck
Coverage gap: quoting errors, deprecated syntax, SC2xxx/SC1xxx bug classes,
command misuse. These will not be checked on this run.
- **shfmt** not found on PATH.
...
List one block per missing tool. Each block names the tool, gives macOS + Debian + RHEL install commands, and enumerates the lint categories that tool would have covered so the user can decide whether to install it or accept the gap.
check-shell does not hard-exit when a tool is missing. It proceeds
to Checks with whichever tools are present. The fail-loud mechanism is
the visibility of the preamble at the top of the report.
Run 14 lints scoped to the detected target shell, plus output from whichever external tools are available. Each lint entry below names the pattern, why it is wrong, severity, and fix guidance.
Script uses a feature unavailable in its declared or inferred target.
Examples: declare -A or mapfile in a bash-3.2-portable script;
[[ ]] or local in a posix-sh script; EPOCHSECONDS in
bash-4+. Severity: fail. Fix: remove the feature or change the
target.
mktemp -t or --tmpdir without portable fallbackGNU mktemp rejects -c, HP-UX requires it, BSD -t differs, and
Solaris lacks mktemp entirely. The only cross-platform invocation is
argumentless mktemp (writes to $TMPDIR). Severity: warn. Fix:
call mktemp with no flags, or fall back with a command -v probe.
posix-sh target contains [[ ]], local, <<< here-strings,
$'...', (( )) arithmetic, arrays, or process substitution.
Severity: fail. Fix: rewrite using POSIX equivalents ([ ],
function-scoped vars, pipes instead of here-strings).
$var without surrounding quotes. Word-splits on IFS, glob-expands
on filesystem contents, and exposes the script to payload-driven
exploits. Severity: fail. Fix: "${var}" unless word-splitting is
deliberate (rare; comment when it is).
for f in $(ls ...) instead of globIterates a command-substituted ls output; breaks on filenames with
spaces, newlines, or leading dashes (BashPitfalls #1). Severity:
fail. Fix: for f in *.ext or find ... -print0 | while IFS= read -r -d ''.
cat file | cmd > file)Truncates the destination before the producer finishes reading it; in
most shells the file ends up empty (BashPitfalls #13). Severity:
fail. Fix: use a temp file or sponge from moreutils.
while | read pipeline variable lossVariables set inside a while ... read loop fed by a pipe are lost
when the loop exits because the right-hand side runs in a subshell
(BashPitfalls #8). Severity: warn. Fix: use process substitution
(while read ...; do ...; done < <(producer)) in bash, or a named
pipe / temp file in POSIX sh.
IFS= without restoreIFS= changed at file scope and never restored leaks into downstream
commands. Severity: warn. Fix: assign IFS only on the line that
needs it (IFS=, read -r a b c <<<"..."), or save + restore in the
function.
find | xargs without -print0 / -0Whitespace in filenames breaks the pipeline. Severity: fail. Fix:
find ... -print0 | xargs -0 cmd, or find ... -exec cmd {} +.
cd X; Y without || guardIf cd fails, Y runs in the wrong directory (BashPitfalls #19).
Severity: fail. Fix: cd X || exit 70 or (cd X && Y) in a
subshell.
[[ ]] with > or < on numerics[[ $a > $b ]] compares as strings, not numbers (BashPitfalls #7).
Severity: fail. Fix: (( a > b )) or [ "$a" -gt "$b" ].
mktemp before cleanup trapmktemp creates the temp file before the trap is installed, so a
signal between the two leaks the file. Severity: warn. Fix:
install the trap first, then call mktemp.
localVariables assigned inside a function leak to global scope when
local is omitted. Severity: warn. Fix: declare with local
(bash) or use a subshell ( ... ) (POSIX sh).
No structured block after the shebang describing purpose, usage, exit
codes, and dependencies. Severity: warn. Fix: add the header per
the build-shell scaffold template.
The script calls exit N (N ≠ 0) for a code not listed in the
header's Exit codes: block. Severity: warn. Fix: document every
non-zero exit in the header, or use sysexits codes (64–78) consistently.
TODO without attributionTODO, FIXME, or XXX without a parenthesized name (per the Google
Shell Style Guide: TODO(mrmonkey): ...). Severity: warn. Fix:
attribute every TODO so readers know who to ask.
PROGNAME driftPROGNAME hardcoded to a string that does not match basename "$0"
or the file's basename. Severity: warn. Fix: PROGNAME="$(basename "${0}")".
<<EOF (unquoted) performs variable expansion inside the heredoc,
which is usually unintended for static help text and can leak shell
state. Severity: warn. Fix: <<'EOF' for literal help text.
echo "Error: ..." without >&2. Errors on stdout poison pipelines
that consume the script's output. Severity: warn. Fix: echo "..." >&2
or printf '...\n' >&2.
Present findings as a table, preceded by the Missing Tools preamble when applicable. Summary count at the top:
N findings across <script path> (X fail, Y warn)
[Missing Tools preamble if any tool absent]
group | lint | finding | line
--------------+------+---------------------------------------------+-----
Portability | P1 | `declare -A` requires bash 4+ | 42
Safety | S1 | Unquoted $INPUT expansion | 57
Safety | S7 | `cd "$WORKDIR"; do_work` missing `||` | 61
Documentation | D1 | Missing top-of-file header | -
When shellcheck, shfmt, or checkbashisms were run, append their
output as a separate section below the 14-lint table (one section per
tool), preserving the tool's native output format so the user can
correlate with upstream documentation.
If zero findings: "Script looks clean (against the 14 lints and the available external tools)."
Offer the follow-up:
"Findings suggest a rewrite may be cheaper than remediation — want to run
/build:build-shellto scaffold a fresh version?"
Only suggest this when the severity mix is heavy on fail-level
structural issues; for a handful of warn findings, the user should
fix in place.
shellcheck should produce a Missing Tools block, not an error
exit. The 14 FX lints run independently of tool availability.posix-sh script produces false negatives. Scope every
lint to the detected or declared target.mktemp-before-trap, bare TODO).fail at fail even when the script is small./build:check-hook,
which handles hook-specific checks (matcher casing, Stop hook loop
risk, jq field paths on tool_input)./build:build-shell when a
rewrite is cheaper than remediation, or leave the user to apply
fixes in place.Receives: path to a shell script (from $ARGUMENTS or elicited).
Produces: a findings table with the Missing Tools preamble when
applicable; read-only — no files modified.
Chainable to: /build:build-shell (when findings suggest rewrite
is cheaper than remediation).
tools
Use when the user wants to "audit a help skill", "review my plugin index", or "verify my help-skill is up to date". Audits a plugins/<plugin>/skills/help/SKILL.md against the help-skill rubric — coverage, freshness, frontmatter fidelity, plus five judgment dimensions and a trigger-collision check.
tools
Use when the user wants to "scaffold a help skill", "add a /<plugin>:help command", or "build a plugin index skill", or wants to give a plugin an orientation surface that lists its skills and common workflows. Produces a SKILL.md at plugins/<plugin>/skills/help/SKILL.md.
tools
Audits pair-level integrity of a primitive-pair (the artifact `/build:build-skill-pair` produces) by walking the four required artifact slots — principles doc, `build-<primitive>/SKILL.md`, `check-<primitive>/SKILL.md`, and the `primitive-routing.md` registration — and reports cross-artifact issues a per-SKILL.md checker cannot see: missing principles doc, divergent principles paths between halves, absent routing registration, missing build→check handoff. Per-half structural compliance with the unified pattern (`check-skill-pattern.md`) is delegated to `plugins/build/_shared/scripts/check_skill_pattern.py`. Use when the user wants to "audit a skill pair", "review a primitive pair", or "validate the skill pair for X". Not for auditing a single SKILL.md — route to `/build:check-skill`. Not for re-distilling a stale principles doc — route to `/build:build-skill-pair`.
testing
Audit a root-level resolver — verify AGENTS.md pointer, managed-region integrity, filing-table coverage against disk, context-table actionability, and trigger-eval pass rate. Use when the user wants to "audit a resolver", "validate routing table", or "find dark capabilities".