fix(guardrails): classify destructive git subcommands as mutating - #2720
Conversation
`ps::git_command_is_readonly` decides whether a PowerShell command that already carries a construct the Bash tokenizer cannot read may SKIP the fail-closed sink under the `readonly-ok` scope. Its subcommand blocklist omitted `clean` and `restore`, so `git clean -fdx` and `git restore .` — both of which destroy uncommitted work irrecoverably — classified as READ-ONLY, violating the library's own OVER-BLOCK, NEVER UNDER-BLOCK invariant. `pull`, `add`, `apply`, `branch`, `config`, `mv`, `rm`, `switch`, `submodule`, `sparse-checkout`, `reflog`, `gc`, `prune`, `repack`, `update-ref`, `update-index`, `read-tree`, `checkout-index`, `symbolic-ref`, `filter-branch`, `bisect`, `remote`, `replace`, `rerere`, `subtree`, `maintenance` and `merge-file` were missing too. The set is now derived from a predicate stated in the function's own comment rather than remembered, with the interrogators, artifact producers, create-only plumbing and repo-creating forms deliberately omitted and justified. The `-`-excluding token boundary is documented as load-bearing (`--prune`, `ls-remote`, `merge-base`, `--no-merges`, `--tags` still classify read-only), the #1415 `fetch` allowance is preserved with its own residual recorded, and the negative-shape-match residual is documented as deferred to an allowlist inversion. No live exposure today: `block-no-verify` is the only caller passing `readonly-ok`. This lands ahead of any widening of that scope. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ABrALrGBH8fev9QA71kmQW
|
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 4m 23s —— View job Code review complete
This lane covers correctness/maintainability; security findings are scoped to Posted 2 inline comments:
Everything else checked out:
Not independently verified: I could not execute |
|
Claude finished @kyle-sexton's task in 6m 49s —— View job Security review completeScope:
This PR's own stated goal is to close every hyphen-boundary gap where a mutating git subcommand slips through IMPORTANT — The new alternation lists Because the boundary class excludes
The new test coverage doesn't catch this either: block-no-verify.test.sh#L339 is labeled "credential-store write" but actually exercises Exposure today: same caveat the PR itself gives for the whole change — No other security issues found. The rest of the diff (the version bump, CHANGELOG entry, and the large new test block) is consistent, and the regex construction elsewhere (literal-only alternations, boundary-class reasoning, alphabetical grouping) held up under a full alternation-by-alternation trace against git's actual porcelain/plumbing subcommand set. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9c632081ae
ℹ️ 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. |
|
Last security-reviewed head: |
typos 1.47.2 flags `mis` in `mis-classified` as a misspelling of `miss`/`mist`. The hyphenated prefix is correct English, but the gate splits on the hyphen and sees a bare `mis` token. Reworded to `wrongly-classified` rather than adding an allowlist entry: the allowlist is a shared surface and a per-comment exception there would outlive the comment.
|
Claude finished @kyle-sexton's task in 48s —— View job Security review complete
Skip gate hit — no new review performed. Per the skill's skip gate, this head already has a successful security review that still applies: the only change since the last-reviewed SHA ( - # a mis-classified subcommand is observable. Every case below carries a SINK
+ # a wrongly-classified subcommand is observable. Every case below carries a SINKNo new security-relevant surface was introduced, so this lane has nothing further to add against For reference, the prior security review (against |
|
Claude finished @kyle-sexton's task in 1m 2s —— View job Code review complete
Skip gate hit — no new review surface. The only change since this lane's last review ( - # a mis-classified subcommand is observable. Every case below carries a SINK
+ # a wrongly-classified subcommand is observable. Every case below carries a SINKblock-no-verify.test.sh#L255 @ eb64a18 No functional or logic change — For context, this lane's prior review (on |
…and gui as mutating Four under-blocks found in review of this PR, all the same class the PR fixes. The `-`-excluding token boundary is load-bearing (it keeps `--prune` and `--tags` read-only), which means a listed stem never covers its hyphenated relatives. `credential` therefore did not match `credential-cache` or `credential-store` -- both directly dispatchable, both writing credentials to disk or managing a caching daemon, both squarely inside this function's own stated predicate. Same class as the `commit-graph` / `merge-index` / `update-server-info` entries already spelled out below them. `git citool` and `git gui` are commit frontends -- `git help -a` calls citool the "Graphical alternative to git-commit" -- so a commit made through either inherits the session environment, including a disabled hook manager, and must not ride the readonly-ok path. Verified: all four now classify mutating; `status`, `log`, `fetch`, `merge-base`, `rev-list`, `ls-remote` and `--no-merges` still classify read-only, so the boundary is intact. Also corrects the comment's "five tests" count to six.
|
Claude finished @kyle-sexton's task in 1m 39s —— View job Security review completeScope:
Not a skip. Commit Both previously open findings are resolved:
Alphabetical ordering and boundary correctness hold for both new entries in their respective alternation lines, and matching test cases were added in block-no-verify.test.sh#L320-L326 for Not independently executed: as with the prior review round, I could not run No new security issues found in this commit. Both findings from the prior review rounds on this PR are now addressed. |
|
Claude finished @kyle-sexton's task in 1m 26s —— View job Code review complete
Reviewed the delta since this lane's last review ( Both previously-flagged gaps are now closed correctly:
Verified by trace:
No new findings. I did not spot any other gaps in this delta. Not independently verified: I could not execute One pre-existing, unchanged nit for awareness (not a new finding, no action needed): the test label at block-no-verify.test.sh#L350 still reads "credential-store write" while exercising |
|
All three review threads addressed in
Exactly the hyphenated-sibling class the comment already called out for
"five tests" count — corrected to six. Post-change verification, all four now mutating with no read-only regression: The boundary-sensitive cases ( |
…udit-derivability-o12zbc Resolve guardrails CHANGELOG conflict: keep main's 0.28.26 SECURITY fix (#2720) and renumber this PR's residual-docs changes to 0.28.27. Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
Problem
ps::git_command_is_readonlydecides whether a PowerShell command may skip the fail-closed sinkunder the
readonly-okscope. Its subcommand alternation omitted destructive forms, so commandsthat irrecoverably destroy uncommitted work classified as READ-ONLY.
This violates the library's own stated invariant, from the header comment of
plugins/guardrails/lib/powershell/ps-command.sh:On
mainthe alternation is a single line:cleanandrestoreare absent — and so are a dozen more.Measured, not asserted
A harness sourcing the library directly and calling
ps::git_command_is_readonlyon 33 commands,run against both trees on the same machine:
origin/mainThe 14 forms that classify as read-only on
maintoday:git clean -fdx·git restore .·git restore --staged --worktree .·git switch main·git rm -r src·git mv a b·git add .·git pull·git branch -D feature·git update-ref refs/heads/main HEAD·git sparse-checkout set src·git submodule update --init·git gc --prune=now·git filter-branch --forceAll 14 are fixed. All 12 genuinely read-only forms in the harness —
status,log,fetch,rev-list,merge-base,diff,show,describe,ls-files,cat-file,blame,rev-parse— still classify read-only, so this does not trade a fail-open for a fail-closed regression.
Approach
The set is derived from a stated predicate rather than accumulated by example: a subcommand is
not read-only when it can create, modify, delete or overwrite working-tree or index content;
create, delete, move or rewrite local refs or history; alter the stash, configuration or repository
administrative state; or publish to a remote.
Deliberate omissions (interrogators, artifact producers, create-only plumbing, repo-creating forms)
are justified in the function's own comment rather than left implicit.
Two properties are load-bearing and documented in place:
[[ =~ ]]pattern is the one bash construct whose quote-removal is genuinely version-sensitive, and a
predicate that silently stops matching here fails OPEN.
--excluding token boundary is intentional.--prune,ls-remote,merge-base,--no-mergesand--tagsmust keep classifying read-only; the boundary is what prevents a flagfrom matching a subcommand name.
The
fetchallowance from #1415 is preserved, with its own residual recorded. Thenegative-shape-match residual — a sink-routing construct can still hide a subcommand, e.g.
git ('cle'+'an')— is documented as deferred to an allowlist inversion rather than silentlycarried.
Exposure and ordering
No live exposure today.
block-no-verifyis the only caller passingreadonly-ok, and itsown path does not reach these forms in practice.
That is precisely why this lands now: it is the ordering prerequisite for any change that
widens
readonly-okusage. Widening that scope before this fix would open all 14 forms at once.Related
readonly-okscope this function gates.0.28.25CHANGELOG entry sits directly below this one; preserved intact by therebase.
disk-hygiene's belt, merged separately.No linked issue. The defect was found by direct source audit rather than an operator report,
and no open issue covers it; filing one to immediately close it would add tracker noise without
adding information. The full measured evidence is in this PR body.