fix(guardrails): defer unparsable PowerShell commits to the guards that can block them (#1858) - #1860
Conversation
…at can block them (#1858) `block-noncanonical-commit` blocked `ps::classify_git_command` rc 2 — "not faithfully tokenizable, and something git-shaped is in there" — with a message naming a commit shape it never got to read, while `block-dangerous-git` blocked the same input with a message describing what was actually observed. The rc-2 arm now defers, taking the same `exit 0` the sibling content gate `block-convention-violation` already takes. Both nonzero arms now emit a `powershell-deferred` telemetry form, so a deferral stays distinguishable from an evaluated allow (rc 1 previously exited with no record at all). Residual, stated rather than buried: the two guards retaining the rc-2 block carry their own kill switches, so a config disabling both while leaving this guard on no longer blocks a git-shaped unparsable PowerShell commit. The tests assert every half — the deferral here, a live block from each sibling on the same two inputs matched on the block reason, and the residual itself with both kill switches off — so the deferral cannot silently become a hole. Also corrects five telemetry schemas that described `tool` as always "Bash" and `subject` as always the tokenized `Bash:<first-token>` form, though `hooks.json` registers all five guards on `Bash|PowerShell` and a PowerShell call is never tokenized. Closes #1858 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 1s —— View job Review complete
|
|
Claude finished @kyle-sexton's task in 2m 10s —— View job Security review complete
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d23fcb567a
ℹ️ 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. |
…schemas (#1858) The previous commit's `tool` description correction matched only `block-noncanonical-commit`: the search string spelled the JSON value's inner quotes unescaped, so it never matched the four sibling schemas' on-disk `Always \"Bash\"` form while the adjacent `subject` correction — quote-free — landed in all five. The result was the exact contradiction the change exists to remove: a schema documenting a PowerShell `subject` shape beside a `tool` contract still claiming the guard is Bash-only. Caught by automated review on PR #1860. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 49s —— View job Code review complete
SummaryThis synchronize re-review covers the one new commit since the prior pass: Codex's finding is fully resolved. The prior commit's
Each now reads Verified independently, not just re-read the diff:
No correctness, error-handling, or maintainability issues found. This closes out the one open item from the previous review pass; nothing else changed in the diff. Note: as in the previous pass, I couldn't execute |
|
Claude finished @kyle-sexton's task in 1m 50s —— View job Security review complete
|
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # plugins/guardrails/.claude-plugin/plugin.json # plugins/guardrails/CHANGELOG.md
|
Claude finished @kyle-sexton's task in 59s —— View job Security review complete
|
|
Claude finished @kyle-sexton's task in 1m 16s —— View job Code review complete
SummaryThis Confirming the mechanics still hold at current HEAD (re-verified directly, not from memory):
No new correctness, error-handling, test-coverage, or maintainability issues found — this pass found nothing beyond what the prior two code-review passes already covered, since no PR-scoped file changed. On the premise challenge: unchanged from the prior review — this is an architectural/product decision (fail-closed-per-guard vs. deferred-to-sibling) worth human adjudication per #1809, not a code-review defect. The code as written remains internally consistent, correctly tested, and honestly documents its own residual exposure. Note: as in prior passes, I could not execute the test suite directly in this environment; verification here is via source/diff inspection, not by running |


Summary
Stage 2 of #1809's recorded ordering decision, filed as #1858.
block-noncanonical-commitblockedps::classify_git_commandrc 2 — "not faithfully tokenizable, and something git-shaped is in there" — with a message naming a canonical commit shape it never got to read, whileblock-dangerous-gitblocked the same input with a message describing what was actually observed. The rc-2 arm now defers, taking the sameexit 0the sibling content gateblock-convention-violationalready takes; both nonzero arms collapse into one deferral.The deferral emits a new telemetry
formvalue,powershell-deferred, so it stays distinguishable from an evaluated allow — rc 1 previously exited with no telemetry record at all, which would have made the change invisible in a fleet.Also corrects five telemetry schemas (
block-noncanonical-commit,block-no-verify,block-dangerous-git,block-hook-bypass,flag-commit-pr-skill-bypass) that describedtoolas always"Bash"andsubjectas always the tokenizedBash:<first-token>form.hooks.jsonregisters all five onBash|PowerShell, each emits the payload's realtool_name, andhook::extract_bash_subjectreturns the bare tool name for a PowerShell call rather than tokenizing it. Pre-existing drift on the same telemetry surface this change touches; descriptions corrected, no payload change.Premise challenge — recommend adjudicating before merge
This implements #1858 exactly as decided, but the decision's justification does not survive verification. Full write-up posted on the issue; summary here.
The cited precedent is category-mismatched. On
origin/main, four guards callps::classify_git_commandwith identical arguments:block-dangerous-git.sh:1072exit 2block-no-verify.sh:231exit 2block-noncanonical-commit.sh:797exit 2block-convention-violation.sh:375exit 0Three blocking guards hold the fail-closed posture; the one that defers is the content gate. #1858 cites that content gate as precedent for changing a blocking guard.
"Coverage cost is provably zero" is false, not merely overclaimed. Each guard carries its own kill switch.
block_dangerous_git_enabled=false+block_no_verify_enabled=false+block_noncanonical_commit_enabled=truenow allows a git-shaped unparsable PowerShell commit through. Under a default install, and under any configuration retaining either sibling, coverage is unchanged — that narrower claim is what the CHANGELOG states, and the contract test pins the residual at exactly that width so it cannot silently widen.Deferred alternative, named not built. If adjudication keeps fail-closed, the motivating complaint — a double block whose second message names a commit shape the guard never read — is fixable by message accuracy rather than deletion: give
block-noncanonical-commitandblock-no-verifyan unparsable message describing what was observed, the wayblock-dangerous-gitalready does. Zero coverage change.Test plan
plugins/guardrails/hooks/block-noncanonical-commit.test.sh— 172 passed, 0 failed. New coverage:exit 0here, relabelled "deferred — classifier rc 2" (not "allowed" — the semantic is deferral);block-dangerous-gitandblock-no-verify: exit 2 and stderr matching the unparsable reason, so a sibling blocking these for an unrelated reason cannot keep the test green while the coupling breaks;block-dangerous-git319/0,block-no-verify112/0,block-convention-violation31/0,flag-commit-pr-skill-bypass28/0. Captured before the schema-only follow-up round, and unaffected by it since no script changed:block-hook-bypass211/0,secret-pattern-detection42/0,hardcoded-path-check72/0.e35deb47lands thetooldescription fix in the four sibling schemas: the first commit's search string spelled the JSON value's inner quotes unescaped, so it matched onlyblock-noncanonical-commitwhile the quote-freesubjectcorrection landed in all five. Caught by Codex review on this PR; verified by re-parsing all five schemas.shellcheck -xclean on all three touched shell files.scripts/check-changelog-parity.sh--check,--check-bump origin/main,--check-order— all pass.scripts/check-changed-skills.sh origin/main— no skills touched.markdownlint-cli2clean on the CHANGELOG; all 31 telemetry schemas re-parse as valid JSON.Related
block-hook-bypass.shwrite-bypass-leak closure must not merge before fix(typos-format,markdown-format): single-writer default ends the unconditional rewriter race #1859. That closure is a separate item, not touched here — this PR's whole scope is the rc-2 deferral, so the constraint does not gate it. fix(typos-format,markdown-format): single-writer default ends the unconditional rewriter race #1859 is still open; the write-leak work remains gated on it.env -S '-i git commit -m x'bypasses every git guard (fail-open, shared with main) #1814/guardrails: hook::git_resolve_index does not peel sudo option clusters, sosudo -bD <dir> git …loses the wrapper chdir (fail-open, shared with main) #1811/guardrails: block-convention-violation passes its whole argv to effective_dir, so a wrapper's -C hides a persisted alias (fail-open, shared with main) #1810, branchfix/1814-wrapper-argv-resolver) also editsblock-noncanonical-commit.shandblock-convention-violation.sh. Different code path (hook::git_resolve_indexrc 2, notps::classify_git_commandrc 2) and non-overlapping hunks. Worth noting: that lane reaches the opposite conclusion on its neighbouring rc-2 surface, writing the doctrine explicitly intoblock-dangerous-gitandblock-no-verify— "Each blocking guard holds this posture itself (per-hook kill switches mean none may delegate it to a sibling)." Independent corroboration of the premise challenge above. Based onorigin/mainonly.