fix(lint): lint-skill.sh recognizes read as a variable binding, not just VAR= - #2490
Merged
Conversation
… just VAR= Check 1 (cross-fence variable usage) only tracked VAR=value assignments, so a var re-bound via `read`/`read -r VAR1 VAR2` in a later block looked identical to a stale reference leaking from an earlier block. This false- flagged fixer/SKILL.md's own I4 integrity check, where a loop-local $COUNT (bound via `read`) collides in name only with the unrelated batch-size $COUNT set in Phase 0. Closes #2344
Contributor
Greptile SummaryThe PR updates the skill linter to recognize variables bound by Bash
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Reviews (5): Last reviewed commit: "fix(lint): replace per-line subprocess p..." | Re-trigger Greptile |
Greptile review on PR #2490: the read-binding scan treated every uppercase token after `read` as a bound destination, so a quoted prompt string (-p "..."), a here-string/redirection operand, or a value-taking flag's own argument (-t/-u/-d/-n/-N) could contain an unrelated uppercase word and get misread as a binding — silently suppressing a genuine cross-fence reference to that word elsewhere in the block. Now strips redirections, quoted arguments, and value-taking flags' own arguments before extracting destination names, and excludes any `$`-prefixed token (a reference, not a binding). Filed #2491 for a separate, pre-existing false-negative in the reference-checking side (a blanket "read " substring exemption) that predates this PR and is out of scope here.
Greptile round 2 on PR #2490: -i takes a value argument just like -p/-t/-u/-d/-n/-N, but was missing from the value-taking-flag strip list, so an uppercase -i argument (readline initial text) could still be misread as a bound destination. -a is deliberately left out of the strip list — its argument is the array actually being read into, a genuine destination, not a value.
…ng flag Greptile round 3 on PR #2490: the strip regex only matched a standalone single-letter flag (-i FOO), not a combined cluster like -ei FOO where -e takes no argument but the trailing -i does. Matches any -[letters] cluster whose LAST letter is a value-taking flag, so its following token is stripped as that flag's value rather than extracted as a destination. -a stays excluded even combined (-ra ARR still keeps ARR as the genuine array destination).
Windows CI failure on PR #2490: a new test now runs lint-skill.sh directly against the real fixer/SKILL.md (by far the largest SKILL.md in the repo, ~850 non-comment bash-block lines) and timed out at the default 30s testTimeout. Check 1's assignment-collection loop forked an echo|grep|sed pipeline unconditionally for every such line — a few seconds on macOS/Linux, but Windows process creation is ~100x slower per this file's own header comment, so a file this size was already at a latent performance cliff pre-existing this PR; a direct per-file test is what first tripped it. Replaces the pipeline with a pure-bash extraction loop ([[ =~ ]] + parameter expansion), matching the file's stated design (every OTHER check already avoids subprocess forking for this reason). Cuts wall time against fixer/SKILL.md from ~4.0s to ~0.95s on macOS. Verified byte-identical error/warning counts across every SKILL.md in the repo before and after. Also bumps the new real-file test's own timeout to 120s as a safety margin, since Windows fork overhead is inherently harder to predict precisely than to bound generously.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
lint-skill.sh's Check 1 (cross-fence variable usage) only recognizedVAR=valueas a variable assignment.read/read -r VAR1 VAR2 ...binds variables just as validly, but with no=after the name.fixer/SKILL.mditself: its I4 integrity check re-derives a per-line$COUNT(expected occurrence count) viawhile IFS=$'\t' read -r F COUNT LINE; do ..., which collides in name only with the unrelated batch-size$COUNTset up in Phase 0. Since each```bashfence is a separate bash invocation, there's no real collision at runtime — but the linter didn't recognize the later block's own freshread-bound$COUNT, so it flagged the block's own local variable as if it were a stale cross-fence reference.read/read -rtargets as variable bindings (sameVAR_BLOCK/REASSIGNEDbookkeeping used forVAR=), scoped to the same block via aregister_varhelper shared by both detection paths.Verified this doesn't mask genuine cross-fence bugs: a var assigned in one block and referenced in a later block without any
read/=rebinding in that later block is still correctly flagged. Ran the patched linter against everySKILL.mdin the repo and diffed error/warning counts against the pre-fix version — onlyfixer/SKILL.mdchanged (2 errors → 0), every other skill's output is byte-identical.Test plan
tests/unit/lint-skill-read-binding-2344.test.ts: does-not-flag (read-rebinding case), still-flags (genuine leak), still-flags (leak past an unrelatedreadin the same block), and a direct assertion thatfixer/SKILL.mditself produces no$COUNTerrornpm run lintcleandiff-impact --staged— no function-level impact (shell script + new test file).claude/skills/*/SKILL.mdbefore/after — no regressionsCloses #2344