Repository navigation
Fail Closed on Words the Shell Rewrites Before Git Reads Them in the Write Guard - #2979
Conversation
…Write Guard The push and include pre-filters also read each command with its quotes and backslashes removed and git in any case, so a quote-split push word, a quote-split include key, and an uppercase GIT reach the checks. A new deny fails closed where a run-time word sits where a git command's subcommand goes, among a push's words, or among a commit's global options or arguments, and where a comment sits among a push's words. A directory option's value, a double-quoted commit option value, a commit path after --, and a redirection target are left alone. Name the cleanup subcommand beside reply in the managed pr_review.py permission rule's description. Closes #2959 Closes #2961 Closes #2968 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A commit option value is split-free only where its quote-keeping token shows every expansion inside double quotes, or where it is one closed $(...) word. A git with no subcommand word denies an unquoted run-time option value. An abbreviated long value option takes the next word as its value. A brace expansion is a run-time word. A redirection target spanning several words, which the push and bypass readings stop at, denies. A process substitution no longer ends the scanned command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Rewrite Deny A brace expansion counts wherever its braces and separator are unquoted, whatever quoted parts sit between them. On any git command, a run-time global option word that word splitting may split denies, and so does a brace expansion in a directory option's value. A redirection where the subcommand goes denies. The README claims now name only where the deny applies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file-descriptor number before a redirection where git's subcommand goes, as in `git 2>/dev/null push`, now denies like an unnumbered one. A redirection after an option that ends git first, such as `--version`, stays allowed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A terminal option such as `--version` exempts a redirection where the
subcommand goes only where it is git's first word, since another
option may take it as its value. A `{name}` file descriptor before a
redirection there denies like a number does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #2979 +/- ##
==========================================
Coverage ? 60.53%
==========================================
Files ? 17
Lines ? 8904
Branches ? 0
==========================================
Hits ? 5390
Misses ? 3514
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate guard correctness issues block approval.
2 open findings
What changed in this PR
This PR hardens the Git write guard against shell-rewritten words and updates managed permission documentation.
Changes:
- Adds detection for runtime expansions, comments, redirections, and quote-split Git commands.
- Expands safety documentation and self-tests.
- Documents
pr_review.py cleanuppermission coverage.
Unresolved review findings remain in gh-write-guard.py: two critical P1 issues and three moderate P2 issues.
| File | Description |
|---|---|
host-setup/agent-safety/README.md |
Documents new guard behavior. |
host-setup/agent-safety/claude/README.md |
Updates safety and permission documentation. |
host-setup/agent-safety/claude/install.py |
Documents cleanup permission coverage. |
host-setup/agent-safety/claude/gh-write-guard.py |
Implements shell-rewrite detection and self-tests. |
🧠 Review effort: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
A redirection right after a value-taking global option, as in `git -c > a core.hooksPath=/dev/null commit`, was read as that option's value, while the shell pairs the option with the word after the target. It now denies like one where the subcommand goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
The scan for where a git command ends now carries the quote mask, so a quoted parenthesis or separator no longer ends it before a later run-time word. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
…de-Override and Shell-Rewrite Denies to Main (#2980) - #2970 adds a `cleanup` subcommand to `scripts/pr_review.py` that runs the merged-feature-branch verify-then-delete in code. It reads the head branch from the merged pull request, refuses a base other than `develop`, a head from another repository, a protected or default branch, and a repository outside the registry, reads the ref by its qualified name, deletes only when its tip matches `headRefOid`, and confirms the delete by reading the ref back. `drive-pr` step 4 and `merge-and-release` step 8 now call it, with the hand form kept as the fallback - #2974 makes `gh-write-guard` deny an `include.path`/`includeIf` override on a commit or push, on a command that runs other git commands (`rebase`, `submodule`, `bisect`, `filter-branch`, `difftool`, `mergetool`), and on any command git does not build in. It reads an `eval` argument holding an include key as the command it runs, and keeps a local branch name such as `release/1.0` whole when checking it against the rule set. A differential against the previous guard found zero allow flips, and the known new over-denials are listed in that pull request - #2979 makes `gh-write-guard` read quote-split words and an uppercase `GIT` through the existing push and include checks, and fail closed on a run-time word (an expansion, a substitution, a backtick, a brace expansion) where the subcommand goes, among a push's or a commit's global options or arguments, on a redirection before the subcommand, and on an unquoted comment among a push's words. The managed permission rule's description now names `pr_review.py cleanup`. A differential against the previous guard found zero allow flips, and the known new over-denials and the gaps left open (#2976, #2977, #2978) are listed in that pull request After this merges, the host-safety installer payload changes, so the maintainer runs the installer after promotion. Closes #2967 Closes #2958 Closes #2960 Closes #2962 Closes #2959 Closes #2961 Closes #2968 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a guarded cleanup option for merged pull request branches. It verifies that the branch is eligible and unchanged before deletion, then confirms the result. * **Security** * Strengthened protections against potentially unsafe Git commit and push commands, including attempts to bypass configured safeguards. * **Documentation** * Updated guidance for branch cleanup, safety checks, and the outcomes reported when cleanup cannot proceed. <!-- end of auto-generated comment: release notes by coderabbit.ai -->

Summary
This closes the write guard's gaps where the shell rewrites or drops a word before git reads it, in
host-setup/agent-safety/claude/gh-write-guard.py, and namespr_review.py cleanupin the managed permission rule's description. Each guard fix adds a reading or a deny beside develop's and replaces none of develop's readings.GIT(Read Words the Shell Rewrites Before a Push in the Write Guard #2959). The push and include pre-filters also read each command with its quotes and backslashes removed, withgitmatched without case. Sogit p''ush origin develop,git "pu"sh ...,git p\ush ...,git -c 'inc''lude.path=/example/config' p, andGIT push ...reach the existing push and include checks, which already read the joined tokens. The same widened match is used inside_unreadable_pushforeval, wrapper,-c, and command-running arguments._rewritten_git_words, fails closed rather than modeling the expansion. A run-time word is a$, a backtick, a brace expansion, or a lone parenthesis run from$(. It denies:git -c core.hooksPath=/dev/null `true` commitgit push origin $(echo develop)or"$B"----version, and a redirection target spanning several words on a push or commit, since the push and bypass readings stop at its parenthesisgit push # feature/yon a develop checkout is no longer read as an explicit push offeature/y. A comment on a commit only adds words the guard reads, which can only add a deny, so it is left alone.MANAGED_PERMISSIONSand the Claude kit README namecleanupbesidereply, and say every check for the delete lives inside the subcommand because the:*rule approves any arguments. No behavior changes ininstall.py.Only a git word in a command's position counts, so
grep -rn git "$W"is no git command, and a git word after an unquoted(counts, so<(git ...)is read. The common agent shapes still allow: a heredoc-m "$(cat <<'EOF' ...)"message (with apostrophes, backticks, braces, or one unbalanced double quote in its body),-m "$MSG",--author="$A",git -C "$W" push origin feature/x, unquotedgit -C $W commit/push, andgit -C "$W" log/status/diff.Requirement 4 in
host-setup/agent-safety/README.mdand the guard bullet in the Claude kit README state the new denies.Differential Against Develop's Guard
Develop's guard (the merge base, e98def1) and this branch's guard were loaded side by side and run over the same calls.
push/commit/include/git/develop(uppercase and mixed-casegitamong them), run-time refspecs ($(...), backtick,$B,"$B",${B},$'...', an empty expansion, a substitution holding a;), run-time subcommands, run-time global options (-c "$X",`true`,$(true),-C "$W",-C $W,-C $(pwd),--git-dir=,--work-tree=,$OPTS), comments, run-time commit arguments, and redirections. The wrappers are plain,bash -c,sh -c ... _ develop,evalquoted and unquoted, a leadingcd,&&,;,timeout, a subshell,x=$(...), andgrep -rn. Sixteen shapes the issues name are added whole.feature/example,release/1.0, andx/develop, each with a real upstream, under six rule sets, one protecting only the stripped names1.0andexample. That is 441,432 calls per guard with no current-branch seam.classifycall the branch self-test makes (894), with each pinned command list mirrored into develop's module.Allow flips (develop denies, this branch allows): zero in every mode. Crashes: zero.
Every deny flip, by the reason the branch gives, from real-repository mode's 7,303 distinct flipped commands:
git $S,git $(echo push),git `true` commit,git -C $(pwd) status)."$F"before--,-m $M,$(echo --no-verify),--author=$A).evalor wrapper argument, or under an include override.2>$(mktemp)).-c "$X",-c user.email=$E).rebase --execargument.Seam mode gives the same classes over 7,561 distinct commands. Rule 6 gives 3,009, the same classes less those rule 6 already denies on develop. The self-test replay flips are exactly the 37 new discriminating cases. Each new deny case fails against develop's guard, which
_GIT_CASESrun against develop's module confirms. On long commands the branch costs about 1.4s more on a 3,000-line heredoc commit (5.5s against 4.0s) and about 1.2s more on 2,000 chainedgit -C "$W" statuscommands.Known New Over-Denials
git push origin "$B"or"$(git branch --show-current)".git $S,git "$@", andgit -C $(pwd) status.git -C $Wwith no subcommand word, orgit --namespace $X log. Any run-time global option on a push or commit other than a directory value, such asgit -c user.email="$E" commit.-m $MSG,--author=$A, or-m $(cat file), and a quoted one before--that no option takes, such asgit commit -m x "$F".git push origin feature/x # note.2>$(mktemp).git 2>/dev/null status, unless git's first word is a terminal option such as--version.-mvalue whose quote-keeping token cannot be read, other than one closed"$(...)"word, when some other part of the command breaks that token reading.Left Open Deliberately
X='. -c core.hooksPath=/dev/null'; git -C $X commit, stays read as it stands, since an unquotedgit -C $W commitis a common agent spelling. Filed as Decide Whether an Unquoted Directory Option Value Fails Closed in the Write Guard #2977. A brace expansion in a directory value denies.`git push ...`or a run-time command wordG=git; $G push, is filed as Read a Backtick-Opened or Run-Time Git Command Word in the Write Guard #2976.--no-verify, as ingit commit --no-veri, is outside this class and filed as Read an Abbreviated --no-verify on a Commit or Push in the Write Guard #2978.Local Review
Eight local strict-review passes ran at the
frontiertier, each recorded. None found an allow flip against develop. Each of the first five found gaps in this change's own fix, fixed in the commits after the first:--as their value{name}forms included--versionexempting a redirection, now only as git's first word, which also clears thegit --version >/dev/null 2>&1over-deny an earlier commit introducedThe sixth pass raised one deny-reason wording mismatch, fixed in a commit the seventh pass confirmed changes that string alone. The eighth pass covered the fix for Copilot's quoted-operator finding and found no open issue. Declined:
_opaque_cd's run-time predicate would replace a develop reading, since its exclusions differ.git -C "$W" --version >/dev/nulldenies, since the terminal-option exception holds only as git's first word. Agents writegit --versionfirst, so it is a stated over-deny.No local finding is open.
Verification
gh-write-guard.py --selftest: SELFTEST PASS, 37 new discriminating cases and 12 new controls.ruff check,ruff format --check,mypy,python3 -m unittest discover -s tests,spec/audit.py --selftest,test_install.py,build_dist.py --check,repo_gate.py(default,eol,eol-coverage),prose_lint.pyon the changed files and--diffagainst the merge base,spec/validate.py, anddocker_lint.pyall pass. The only prose finding iscomment-added, for theinstall.pycomment Name the cleanup Subcommand in the Managed pr_review.py Allow Rule's Description #2968 asks for and oneclassifycomment, so this pull request carries thecommentslabel.After merge, the host-safety installer payload changes. The installer is not run here, and the maintainer runs it after promotion.
Closes #2959
Closes #2961
Closes #2968
🤖 Generated with Claude Code