Skip to content

Fail Closed on Words the Shell Rewrites Before Git Reads Them in the Write Guard - #2979

Merged
ptr727 merged 8 commits into
developfrom
feature/guard-shell-rewrites
Oct 14, 2026
Merged

ptr727 merged 8 commits into
developfrom
feature/guard-shell-rewrites

Conversation

@ptr727

@ptr727 ptr727 commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

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 names pr_review.py cleanup in 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.

  • Quote-split words and uppercase 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, with git matched without case. So git p''ush origin develop, git "pu"sh ..., git p\ush ..., git -c 'inc''lude.path=/example/config' p, and GIT push ... reach the existing push and include checks, which already read the joined tokens. The same widened match is used inside _unreadable_push for eval, wrapper, -c, and command-running arguments.
  • Run-time words (Read Words the Shell Rewrites Before a Push in the Write Guard #2959, Fail Closed on a Comment or a Substitution in a Push or Commit's Own Words in the Write Guard #2961). A new deny, _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:
    • a run-time word where any git command's subcommand goes, which covers a backtick word ending the global options, as in git -c core.hooksPath=/dev/null `true` commit
    • a run-time global option word that word splitting may split, on any git command, or before no subcommand word at all
    • any run-time word among a push's or a commit's global options other than a directory option's value
    • a run-time word among a push's arguments, as in git push origin $(echo develop) or "$B"
    • a run-time word among a commit's arguments, other than a commit option's value whose expansions sit inside double quotes, or a path after --
    • a redirection where the subcommand goes or among the global options, unless git's first word is a terminal option such as --version, and a redirection target spanning several words on a push or commit, since the push and bypass readings stop at its parenthesis
  • Comments (Fail Closed on a Comment or a Substitution in a Push or Commit's Own Words in the Write Guard #2961). An unquoted comment among a push's words denies, so git push # feature/y on a develop checkout is no longer read as an explicit push of feature/y. A comment on a commit only adds words the guard reads, which can only add a deny, so it is left alone.
  • The managed permission rule (Name the cleanup Subcommand in the Managed pr_review.py Allow Rule's Description #2968). The comment above MANAGED_PERMISSIONS and the Claude kit README name cleanup beside reply, and say every check for the delete lives inside the subcommand because the :* rule approves any arguments. No behavior changes in install.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, unquoted git -C $W commit/push, and git -C "$W" log/status/diff.

Requirement 4 in host-setup/agent-safety/README.md and 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.

  • Corpus: 18,393 constructed commands. They cross 35 heads with 8 mutation families and 12 wrappers. The heads are 18 push forms, 9 commit forms including two heredoc messages, and 8 other git commands. The mutation families are quote splits of push/commit/include/git/develop (uppercase and mixed-case git among 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, eval quoted and unquoted, a leading cd, &&, ;, timeout, a subshell, x=$(...), and grep -rn. Sixteen shapes the issues name are added whole.
  • Real-repository mode: each command in a constructed repository's develop checkout and in worktrees on feature/example, release/1.0, and x/develop, each with a real upstream, under six rule sets, one protecting only the stripped names 1.0 and example. That is 441,432 calls per guard with no current-branch seam.
  • Seam mode: the same corpus under 6 current-branch seams and 4 rule seams (441,432 calls).
  • Self-test replay: every classify call the branch self-test makes (894), with each pinned command list mirrored into develop's module.
  • Rule 6: the corpus with a primary-checkout lookup (18,393 calls).

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:

  • 2,721: a run-time word where the subcommand goes (git $S, git $(echo push), git `true` commit, git -C $(pwd) status).
  • 1,485: a quote-split or uppercase push now read by the existing push check, denied as a pull-request, force, delete, or unresolved-branch push.
  • 1,424: a run-time push argument.
  • 764: a comment among a push's words.
  • 320: a run-time commit argument ("$F" before --, -m $M, $(echo --no-verify), --author=$A).
  • 237: a quote-split push or include key now read inside an eval or wrapper argument, or under an include override.
  • 180: a redirection target spanning several words (2>$(mktemp)).
  • 170: a run-time global option word on a push or commit (-c "$X", -c user.email=$E).
  • 2: a quote-split push inside an inline alias value or a rebase --exec argument.

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_CASES run 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 chained git -C "$W" status commands.

Known New Over-Denials

  • Any run-time push refspec or remote, such as git push origin "$B" or "$(git branch --show-current)".
  • Any run-time word where a git command's subcommand goes, on any command, such as git $S, git "$@", and git -C $(pwd) status.
  • An unquoted run-time global option value that may split, such as git -C $W with no subcommand word, or git --namespace $X log. Any run-time global option on a push or commit other than a directory value, such as git -c user.email="$E" commit.
  • An unquoted run-time commit argument, such as -m $MSG, --author=$A, or -m $(cat file), and a quoted one before -- that no option takes, such as git commit -m x "$F".
  • Any comment among a push's words, such as git push origin feature/x # note.
  • A redirection target spanning several words on a push or commit, such as 2>$(mktemp).
  • A redirection before the subcommand, such as git 2>/dev/null status, unless git's first word is a terminal option such as --version.
  • A -m value 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

Local Review

Eight local strict-review passes ran at the frontier tier, 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:

  • a quoted part before an unquoted expansion in a commit value
  • a run-time option value supplying the subcommand, on any git command
  • abbreviated value options taking -- as their value
  • brace expansion, quoted parts inside a brace included
  • a multi-token redirection target, and a redirection where the subcommand goes or among the global options, numbered and {name} forms included
  • a terminal option such as --version exempting a redirection, now only as git's first word, which also clears the git --version >/dev/null 2>&1 over-deny an earlier commit introduced

The 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:

  • Sharing _opaque_cd's run-time predicate would replace a develop reading, since its exclusions differ.
  • git -C "$W" --version >/dev/null denies, since the terminal-option exception holds only as git's first word. Agents write git --version first, so it is a stated over-deny.
  • Sentence-length findings are declined as style, since that check is in no gate.

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.py on the changed files and --diff against the merge base, spec/validate.py, and docker_lint.py all pass. The only prose finding is comment-added, for the install.py comment Name the cleanup Subcommand in the Managed pr_review.py Allow Rule's Description #2968 asks for and one classify comment, so this pull request carries the comments label.

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

ptr727 and others added 5 commits October 13, 2026 21:15
…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>
Copilot AI lite review requested due to automatic review settings October 14, 2026 05:22
@ptr727 ptr727 added the comments Permits the comment lines the pull request adds or edits, which the prose gate otherwise refuses label Oct 14, 2026
@ptr727

ptr727 commented Oct 14, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head 6d6ddb6a97879d4d6b528c423d6f81b88b6c6c14, the content this pull request carries at that commit against develop, and it recorded 1 finding.

@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Repository: ptr727/ProjectTemplate/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f169edce-441d-4cb1-84bd-9c0f749fdc70
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Oct 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (develop@e98def1). Learn more about missing BASE report.

Additional details and impacted files
@@            Coverage Diff             @@
##             develop    #2979   +/-   ##
==========================================
  Coverage           ?   60.53%           
==========================================
  Files              ?       17           
  Lines              ?     8904           
  Branches           ?        0           
==========================================
  Hits               ?     5390           
  Misses             ?     3514           
  Partials           ?        0           
Flag Coverage Δ
python-3.13 60.53% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 cleanup permission 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.

Comment thread host-setup/agent-safety/claude/gh-write-guard.py
Comment thread host-setup/agent-safety/claude/gh-write-guard.py
ptr727 and others added 2 commits October 13, 2026 22:32
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>
@ptr727

ptr727 commented Oct 14, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head 2bf4508b46c6f327d2aa13fb13c3713d51922dee, the content this pull request carries at that commit against develop, and it recorded 0 findings.

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>
@ptr727

ptr727 commented Oct 14, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head a95015be75d554051e5396e55a5b6f76ef9e4c91, the content this pull request carries at that commit against develop, and it recorded 1 finding.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved guard issues include a critical command-substitution inspection gap.

1 open finding
2 resolved since last review

🧠 Review effort: Lite

Comment thread host-setup/agent-safety/claude/gh-write-guard.py
@ptr727
ptr727 merged commit 4a0370f into develop Oct 14, 2026
14 checks passed
ptr727 added a commit that referenced this pull request Oct 14, 2026
…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 -->
@ptr727
ptr727 deleted the feature/guard-shell-rewrites branch October 14, 2026 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comments Permits the comment lines the pull request adds or edits, which the prose gate otherwise refuses

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants