fix(scripts): reject Windows-dialect contract roots and close three shell-portability token gaps - #2064
Conversation
Unverified batch preserved from a session that ended before its findings were falsified against origin/main. Touches check-contract-slice-prune, the shell-portability test suite, and the portability token list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Claude finished @kyle-sexton's task in 1m 53s —— View job Security review complete
No security issues found. Scope: this PR touches only repo-level What I looked at specifically:
No |
|
Claude finished @kyle-sexton's task in 8m 39s —— View job Review complete
Verification note: Bash execution (running the test suites / gates directly) required approval this lane couldn't grant, so I could not independently execute Findings1. The shipped token for But the dedicated unit test block for ( )That local
Contrast with the cluster ( Given how deliberately this PR pins down PRE/POST behavior for every other claim (including guarding against its own self-gating trap), this looks like an oversight rather than an intentional gap — worth adding the same three separator-scoping cases ( What checked out
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 16f87dd208
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Claude has reviewed this PR 3 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
The gap class excludes `(` and `)` alongside the separators, but the comment claimed only separators. Record the exclusion, the converse catch it buys, and the process-substitution blind spot it costs — a tradeoff the date/stat/mktemp tokens already carry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 13s —— View job Security review complete
No security issues found. Scope confirmed via What I checked specifically:
No |
|
Claude finished @kyle-sexton's task in 4m 6s —— View job Review complete
Verification note: Bash execution (running the gate/test suites directly) required approval this lane couldn't grant, so — like the prior review pass on this PR — I could not independently execute Findings (posted inline)
What checked out
|
…call Narrowing the sed command gap to stop at a bare paren also stopped it at a process substitution, which is not a command of its own -- the shell folds it into the word it touches. `sed -f <(gen) -i "$f"` therefore went undetected where the pre-narrowing gap caught it. Readmit exactly that shape; a `)` closing a real subshell still ends the gap, so the converse catch it bought is kept. The `--in-place` unit block was building its local token from the pre-narrowing pattern, so none of its assertions could tell old behavior from new. Point it at the shipped pattern and give both sed tokens separator- and process-substitution-scoping cases, including two backed by the real token list so a hand-copied pattern cannot drift unnoticed again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 34s —— View job Security review complete
No security issues found. Reviewed the diff at HEAD What I checked for logic/trust-boundary issues specifically:
No secrets handling, no credential/token flow, and no authorization/access-control logic in scope — this PR's blast radius is entirely CI-gate correctness (a path-escape lint and a shell-portability lint), not a security boundary. |
|
Claude finished @kyle-sexton's task in 2m 57s —— View job Code review complete
No new correctness, error-handling, or test-coverage issues found. This is my independent read of the diff scoped to the four changed files ( Two previously-open findings are now closedTwo prior review rounds on this PR (mine and Codex's) flagged that the
What I independently traced and confirmed
Scope noteSecurity aspects (the path-traversal-adjacent fix, ReDoS risk in the widened patterns) are out of scope for this lane per |
…dmits (#2085) A one-comment correction to `scripts/shell-portability-tokens.txt:245`, which #2064 merged before this fix landed. Comment-only — no token, no behavior, no test changes. ## What is wrong on main The sed command-gap comment claims `[<>]\([^)]*\)` "readmits exactly that shape and nothing else". It does readmit a process substitution, but `[^)]*` is not recursive, so a process substitution whose body itself contains one — `sed -f <(diff <(a) <(b)) -i "$f"` — stops the gap at the INNER `)` and goes unreported. An ERE cannot balance parentheses; catching that shape needs real parsing, which this grep-level tripwire does not attempt. So the residual is a false NEGATIVE on a rare shape, which is the direction this gate already accepts everywhere else. The defect is the comment asserting a completeness the regex does not have — a reader trusting it would not think to check. ## Provenance Found by the fresh-context verifier on #2064 while auditing the very fix that introduced the sentence. #2064 merged with the overstatement still in place; this carries the correction forward. Prose asserting more than the artifact does has been a recurring defect class on this sweep — it drew CHALLENGE on `context-guard` twice and appeared again as `disk-hygiene`'s R1 — so it is worth correcting rather than leaving as a harmless-looking comment. ## Related Follow-up to #2064. Discharges no review thread; the four findings #2064 addressed are resolved. No linked issue --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Discharges four stranded review findings against the repo-level
scripts/gates. Every finding was reproduced through its real consumer with a pre-fix control before the fix, and each control discriminates — it passes onorigin/main's artifact and changes verdict on this branch's.Finding 1 —
check-contract-slice-prune.shaccepts Windows-dialect absolute rootsPRRT_kwDOTCGFQM6Tz-jR(#1445),scripts/check-contract-slice-prune.sh:119.canonicalize_repo_path()judged absoluteness only in the POSIX dialect (/*). A drive-qualified or backslash-rootedcontract_dirwas therefore read as repo-relative. Git names repo-relative diff paths with/separators and never with a drive qualifier or a raw backslash, so such a root can match no diff path at all —--check-diffreports success over an empty match set, policing nothing. That is precisely the fail-open the gate exists to prevent.The irony is worth stating plainly:
19d736bf(#1445) both discharged the three sibling threads on this file and introduced this one — same harm class, different route in, inside the very function it added.Reproduced with three fixtures —
C:/outside,C:\outside,\\server\share— all of which passed silently under--check-diffbefore the fix. The legitimate-root control fails correctly on the same machinery, so the reproduction discriminates rather than merely erroring.Absoluteness is now judged in both dialects. A backslash anywhere is refused, not only a leading one: it is a separator in the Windows dialect and an escaped character in Git's own output, so no value carrying one is comparable to a diff path. Note
c:outsideis drive-qualified but relative in Windows semantics — it is refused for the same Git-comparability reason, not because it is absolute.Finding 2 — the
--sorttoken missed a fully quoted option wordPRRT_kwDOTCGFQM6T1s2n(#1530),scripts/shell-portability-tokens.txt, filed at:98, live at:173.The pattern demanded whitespace immediately before
--sort, so a quote wrapping the whole option word sat outside the newly added optional-quote position.sort '--sort=version' "$file"andsort "--sort=version" "$file"hand GNU sort the identical argument after quote removal, yet the scanner returned clean.An optional quote is now admitted at both positions.
git tag --sort=version:refnamestays clean for the reason it already did — the WORD boundary still rejects the longerversion:refname.Findings 3 and 4 — the sed tokens, landed together
Both edit the same live token at
:216, so they cannot be split.Finding 3 (
PRRT_kwDOTCGFQM6T1rfG, #1534, filed:117, live at:216and:232): the unrestricted[[:space:]][^\n]*command gap crossed;,&&and|, so a later command's options armed the sed token.sed -n 'p' "$file"; grep -Ei pattern "$file"was reported even though onlygrepreceives-Ei. The reporter also called the second site: "the--in-placepattern has the same issue" — so the:232edit is in scope as filed, not creep. Both gaps now stop at a shell command separator, matching what thedate -d/stat -c/mktemp -ptokens already do. Quoted separators are neutralized before the token matches, so a;inside a sed script stays ordinary data.Finding 4 (
PRRT_kwDOTCGFQM6T1rfI, #1534, filed:117, live at:188and:216): scope is broader than filed. Two tokens readsed -niclean — one keyed on a literal-isubstring, which-nidoes not contain; the other on anEearlier in the cluster. GNU sed 4.9's--helpdocuments-i[SUFFIX]alongside the no-argument short options-n,-b,-E,-r,-s,-u,-z, so any cluster built from those letters and ending iniis the same unsuffixed in-place edit. Premise verified live against GNU sed 4.9:sed -nirewrote a file in place, 3 lines to 1.The two narrower predecessors are therefore consolidated into one token —
-istanding alone and-iending a cluster are the same option. The argument-taking letters-e,-f,-lare deliberately outside the class: GNU accepts their value attached, sosed -eipasses the scriptirather than editing in place.The removed
plain -i (no E) does not double-fire this cluster tokentest goes with the consolidation — with one token there is nothing to double-fire.Verification
Every row below ran through the real consumer,
scripts/check-shell-portability.sh --paths <fixture>, never by hand-runninggrep -Eagainst the token file.PREusesorigin/main's token file viaSHELL_PORTABILITY_TOKENS;POSTuses this branch's.0= gate passes,1= gate reports the construct.--sort=version(4 spellings)sed -n 'p' f; grep -Ei p fsed -n 'p' f && tool --in-place x--in-placesite, same shapesed -ni '/keep/p' fsed -i '' 's/foo/bar/' fsed -i.bak/sed -Ei.bakThe last two rows exist because this change deletes a token. The empty-suffix idiom looks BSD-safe but is not (GNU consumes the empty string as sed's script argument and exits 2), and the attached-nonempty-suffix form is the one genuinely portable spelling. Neither may shift.
The self-gating trap
shell-portability-lintgates this PR, and these edits change the lint that runs against this PR's own diff. A fixture-only check would not have caught a token that newly flags existing legitimate code, so the whole-repo audit was run under both token files and diffed:scripts/check-shell-portability.sh --allwithorigin/main's tokens and with this branch's tokens produce a byte-identical hit set.\b/\w/\s/\Sin four test files);--allexits 1 on both. CI's gate is changed-file scoped, so that debt is not this PR's to carry.scripts/check-shell-portability.sh origin/main, is green: No unexcused GNU-only constructs in 3 shell file(s).Nothing was suppressed and no
portability-ok:marker was added.Changelog parity
Not applicable to this PR, determined by reading the gate rather than assuming.
scripts/check-changelog-parity.shscopes itself to plugins:--checkand--check-bumpglobplugins/*/.claude-plugin/plugin.json, and--check-orderglobsplugins/*/CHANGELOG.md docs/conventions/*/CHANGELOG.md. This diff touches only repo-levelscripts/, which has no plugin manifest, and the repo has no rootCHANGELOG.md.Worth closing the one loose end explicitly:
--check-bumpdoes takeorigin/$BASE_REF, so a diff-scoped gate could in principle fire on a scripts-only diff. It cannot here — the diff scope is used only to detect manifest version changes, and this PR changes no manifest version.Scope
Four hunks, four findings, no unmapped changes. The three sibling threads on
check-contract-slice-prune.sh(all #1429) are already fixed by19d736bf(#1445), confirmed an ancestor of main with pre-fix controls at19d736bf^reproducing both bugs; this PR deliberately contains no fix for them.Related
No linked issue