feat(skill-quality): enforce shell declaration for injection-carrying skills - #883
Conversation
… skills Add check 19 to check-skill.sh: a skill using `!` dynamic-context injections (inline !`cmd` or ```! blocks) with no `shell:` frontmatter FAILs when the injected commands carry detectable bash-only syntax (/dev/null, command -v, a pipe into a Unix text tool) and WARNs when the commands only look portable — static analysis cannot prove portability, and an undeclared bash-only injection silently falls through to the PowerShell tool on a host without Git Bash. A `shell:` declaration is trusted as the author's explicit choice. Add check 20: WARN when an injected command carries no `|| <fallback>` continuation, per the pinned defensive-fallback convention. Both checks scan the injected command text only, never prose or a plain ```bash example. Inline injections are recognized only at line start or after whitespace (per the injection docs), so a mid-token !` such as an inline `#!` code span is not captured. Eight new self-tests cover the FAIL/WARN tiers, fenced-block extraction, the || (not || echo) fallback match, the over-reach guard, and the prose-anchor guard. Closes #865
|
Claude finished @kyle-sexton's task in 4m 41s —— View job PR Review — feat(skill-quality): Check 19 + 20
SummarySolid implementation of a well-reasoned check. The three-way conditional (injection present ∧ no Findings1. The bash-only pipe regex matches git log --oneline || head -20 /dev/stdinwould FAIL Check 19 because 2. INFO note fires on every passing skill-with-injections run [minor — verbosity] When note "shell: declared — dynamic-context injection portability is the author's explicit choice"This outputs See: 3. Check 20 test coverage gap — multi-injection count [minor — tests] Test 26 exercises a single injection without fallback and checks that See: What's correct and well-done
CLAUDE.md alignmentNo issues. This is a new check added to an existing plugin's internal script — no new hardcoded paths, no PII, no secrets, plugin-form-safe. Semver bump is present. The CHANGELOG entry meets the parity requirement. The README catalog and plugin.json description are updated in the same diff. |
|
Claude finished @kyle-sexton's task in 3m 17s —— View job Security Review — PR #883
ScopeReviewed the six files changed in this PR against commit
FindingsNo security issues found in the code added by this PR. Detailed analysis of each candidate surface follows. INJECTIONS extraction loop (L564–L604)Skill file content is read line-by-line with Check 19 — bash-only token scan (L622–L641)The printf 'FAIL: %s\n' "$*"The Check 20 — fallback scan (L649–L655)Pure Test fixtures (check-skill.test.sh L956–L1141)Fixtures are written with Other changed files
Note on a pre-existing pattern (not in this PR's diff)Check 7 (line 304, unchanged by this PR) runs |
Closes #865
What
Two new checks in
plugins/skill-quality/scripts/check-skill.sh, guarding the regression the 2026-07-21 fleet census surfaced (64 skills across 26 plugins using!injections with bash-only constructs and noshell:declaration — silently broken on a Windows host without Git Bash, where injections fall through to the PowerShell tool). PR #860 swept the fleet; nothing prevented regression until now.!dynamic-context injections (inline!`cmd`or```!blocks) and declares noshell:frontmatter, it FAILs on detectable bash-only syntax and WARNs on portable-looking commands.|| <fallback>continuation, per the pinned precompute convention.Eight self-tests cover both tiers, fenced-block extraction, the
||-not-|| echomatch, and two over-reach guards.FAIL/WARN tiering rationale
Portability is not statically decidable, so the tiers split on evidence strength:
shell:— the exact census failure. The bash-only token set is deliberately narrow:/dev/null(PowerShell is$null),command -v(a bash builtin; PowerShell isGet-Command), and a pipe into a Unix text tool with no same-named PowerShell cmdlet (head,sed,awk, …;sort/teeare excluded because PowerShell aliases them). Tight avoids a false FAIL that blocks a PR; anything the set misses degrades to the WARN path — never a false negative that FAILs a portable skill.shell:but the commands only look portable — static analysis can't prove it, so nudge rather than block.shell:is declared: the author has taken explicit responsibility for the shell (see scope boundary below).Check 20 is WARN-only (not FAIL) — the fallback convention is a defensive nicety, not a correctness invariant; injection failure/stderr semantics are undocumented, so this is a nudge.
Why in the plugin's check-skill.sh, not the repo-level check-skill-portability.sh
The repo already has
scripts/check-skill-portability.sh, but that is a flat token-present-in-file → violation grep (its concern is ecosystem/forge/branch agnosticism, e.g. a bareorigin/main). #865 needs a three-way conditional that a flat scanner cannot express: an injection exists ∧ noshell:is declared ∧ a bash-only token sits inside the injected command text (not anywhere in the file — a bash-only token in a plain```bashexample must not flag). That per-skill, structure-aware logic belongs with the other skill-contract checks incheck-skill.sh, which already parses frontmatter and code fences.Scope boundary
A
shell:declaration is trusted wholesale — the check does not validate that the injected commands actually match the declared shell, soshell: pwshwith bash-only commands is intentionally out of scope (per-shell syntax validation is a separate, much larger concern). Both checks scan the injected command text only; inline injections are recognized only at line start or after whitespace (per the injection docs), so a mid-token!`such as an inline`#!`code span in prose is not captured.Version
skill-quality0.7.2 → 0.8.0 (minor — matches check-18's minor-bump precedent for a new check), with the parity-gated CHANGELOG entry and regenerated README catalog in the same diff.Gates run locally (all green)
check-skill.test.sh— 47 assertions pass (39 existing + 8 new)shellcheck --rcfile=.shellcheckrcon both scripts — cleanscripts/validate-plugins.sh— passes (catalog regenerated)scripts/check-changelog-parity.sh --check-bump origin/main— passesmarkdownlint-cli2on changed markdown — cleanRelated
Related but NOT closed by this PR:
shell: bashto the 64 affected skills; this PR is the regression guard that keeps the census failure from returning.check-skill-portability.shagnosticism gate (the sibling "portability" gate this PR deliberately does not extend; see the rationale section above).plugins/playbooks/skills/skill-authoring/reference/precompute-context.md— the pinned precompute / defensive-fallback convention that checks 19 and 20 enforce.