Skip to content

Read a Push's Remote and Branch in the Repository Git Uses in the Write Guard - #3709

Merged
ptr727 merged 4 commits into
developfrom
feature/burndown-3695
Oct 31, 2026
Merged

ptr727 merged 4 commits into
developfrom
feature/burndown-3695

Conversation

@ptr727

@ptr727 ptr727 commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

#3695 asked whether the write guard's requirement 4 reads a push's remote in the repository a --git-dir names. It did not. A check with two constructed checkouts, a with origin github.com/alpha/x and b with origin github.com/beta/y, run from a, showed the guard querying alpha/x for each of these:

  • git --git-dir=<b>/.git push origin :feature/x, and the same with a separate --git-dir value
  • git --git-dir=<b>/.git push origin develop, where beta/y required a pull request on develop and the guard allowed the push
  • GIT_DIR=<b>/.git git push ..., env GIT_DIR=... git push ..., and export GIT_DIR=...; git push ...

-C <b> and -C <b> --git-dir=.git already read beta/y, and --work-tree=<b> reads alpha/x, as git itself does.

  • _push_repo now reads the remote's push URL in the repository git uses: the one a --git-dir, or else a GIT_DIR the push itself carries (as a prefix or through env), names, resolved from the directory after -C.
  • A GIT_DIR set earlier in the text reads as undetermined, since whether git sees it turns on an export or an unset the reading does not track.
  • A -c or --config-env override of a remote. or url. key, a GIT_CONFIG variable in the text, and a --git-dir the reading cannot resolve each leave the repository undetermined. That fails closed for main, master, and develop, as the spec already states for an unresolvable origin.
  • A push naming no refspec now reads its branch in the repository a --git-dir or the push's own GIT_DIR names, an absolute --git-dir even after an unresolved cd. It was left unjudged before, so such a push into a develop checkout passed. One whose repository stays undetermined is still allowed, as the README now states.
  • A URL remote now resolves even where the working directory does not.
  • The self-test gains a live check over two real temporary checkouts, one for each shape's repository and one for each bare push's branch.
  • host-setup/agent-safety/README.md requirement 4 states which repository the remote is read in.

Verification

  • python3 host-setup/agent-safety/claude/gh-write-guard.py --selftest: SELFTEST PASS, each new repository and branch case ok.
  • Local strict review: one pass raised three findings (the bare-push branch, README wording, a self-test case that also passed on the old code). Each is fixed in the second commit. Copilot's round then raised two (an earlier, unexported GIT_DIR trusted, and an absolute --git-dir after an unresolved cd), fixed in the third. The last local pass, over the full diff at the head, raised none.
  • ruff format and check, mypy, tests/test_guard_size.py, and the prose gate on the diff: clean.

Closes on promotion: #3695

🤖 Generated with Claude Code

ptr727 and others added 2 commits October 31, 2026 09:53
Requirement 4 read a push's remote URL in the directory's repository,
so a `--git-dir` or `GIT_DIR` naming another repository had its branch
rules judged against the wrong one. The remote is now read in the
repository git itself uses. A config override of a remote, a
`GIT_CONFIG` variable, and an unresolved `--git-dir` leave the
repository undetermined, which fails closed for a protected default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A push naming no refspec under a `--git-dir` or `GIT_DIR` now reads the
branch checked out in that repository rather than leaving it unjudged.
The README names `--config-env`, a text `GIT_DIR`, and every
`GIT_CONFIG` variable among the overrides the reading does not apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings October 31, 2026 16:58
@ptr727

ptr727 commented Oct 31, 2026

Copy link
Copy Markdown
Owner Author

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

@coderabbitai

coderabbitai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

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

Run configuration
  • 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

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

A critical issue remains because stale or unexported GIT_DIR values can make the guard inspect the wrong repository and apply incorrect owner or branch protections.

1 open issue
What changed in this PR

This pull request makes the write guard resolve push remotes and implicit branches from the repository Git actually uses, including repositories selected through --git-dir and GIT_DIR. It also documents the behavior and expands live self-test coverage.

Changes:

  • Resolve push URLs and refspec-free push branches from the selected repository.
  • Fail closed when repository or configuration resolution is unsafe.
  • Add temporary-checkout tests covering alternate repositories, branches, URLs, and configuration overrides.
  • Document repository-selection behavior in the agent-safety requirements.

Open review findings:

  • [P2, moderate; 1 vote] An absolute --git-dir is treated as unresolved after an unknown cd or -C, potentially denying otherwise valid protected-branch pushes.
  • [P1, critical; 2 votes] Environment parsing can trust a non-exported or unset GIT_DIR, causing the guard to inspect the wrong repository.
File Summary
host-setup/​agent-safety/​README.md Documents repository resolution and fail-closed behavior for push remote and branch checks.
host-setup/​agent-safety/​claude/​gh-write-guard.py Implements repository-aware push analysis and adds live temporary-repository self-test coverage.

Review effort: Lite

@codecov

codecov Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.02%. Comparing base (b30ce55) to head (72078e2).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #3709   +/-   ##
========================================
  Coverage    82.02%   82.02%           
========================================
  Files           27       27           
  Lines         9273     9273           
========================================
  Hits          7606     7606           
  Misses        1667     1667           
Flag Coverage Δ
python-3.13 82.02% <ø> (ø)

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.

ptr727 and others added 2 commits October 31, 2026 10:06
A `GIT_DIR` set earlier in the text reaches git only through an export
the reading does not track, and an `unset` can clear it. So only a
`GIT_DIR` on the push itself, or through `env`, names the repository,
and an earlier one reads as a run-time word. A bare push with an
absolute `--git-dir` now reads its branch after an unresolved `cd`, as
the remote already was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ptr727

ptr727 commented Oct 31, 2026

Copy link
Copy Markdown
Owner Author

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

@ptr727

ptr727 commented Oct 31, 2026

Copy link
Copy Markdown
Owner Author

Copilot's overview on e23b05b listed two findings under Open review findings:, a heading the reader has no spelling for (filed as #3713). The P1 is its thread, fixed in f4ef95b and resolved. The P2, an absolute --git-dir read as unresolved after an unresolved cd or -C, is also fixed in f4ef95b. _push_repo already resolved it, and _bare_push_branch now does too. The self-test pins cd "$X" && git --git-dir=<b>/.git push origin reading develop and the matching remote case reading the other owner.

@ptr727
ptr727 merged commit a081c47 into develop Oct 31, 2026
13 checks passed
@ptr727
ptr727 deleted the feature/burndown-3695 branch October 31, 2026 17:19
ptr727 added a commit that referenced this pull request Oct 31, 2026
Fixes a finding raised on promotion #3715, against the change #3709
made.

## What was wrong

`_launch` in `host-setup/agent-safety/claude/gh-write-guard.py` read
past an `env` launcher without applying it. Constructed scratch
repositories reproduced each shape, two checkouts whose origins name
different owners, one with a pull-request rule on `main`:

- `env -C <dir>`, `env --chdir=<dir>`, and `env --chdir <dir>` left the
push judged in the caller's repository. So `env --chdir=/srv/other
GIT_DIR=.git git push origin main` read the wrong remote and allowed.
- `GIT_DIR=<mine>/.git env -i git -C <other> push origin main` and the
same with `env -u GIT_DIR` read the cleared `GIT_DIR`, so the check
again ran against the wrong repository.
- `env -S '<command>'` and `env -S'<command>'` were never read at all,
so every requirement missed the command, including requirements 1, 3,
and 6.

## What changed

- `_launch` now returns a `_Launch` carrying what an `env` changes.
`-C`/`--chdir` moves the directory the command runs in, composed before
a git `-C`. A run-time directory leaves it undetermined, which fails
closed for `main`, `master`, and `develop` under requirement 4, as #3709
treats an unresolved `GIT_DIR`. `-i`/`-` and `-u`/`--unset` clear what
the command inherits, and `-S`/`--split-string` is split into the words
it runs. A short cluster such as `-iC dir`, an unambiguous long prefix,
and the value-taking `-a` and `-P` read the way `env` reads them.
- `_read` applies those to each command's environment, its own
assignments, and its directory, and passes the moved directory into an
`sh -c` body.
- Self-test cases cover each shape, against the real two-owner checkouts
for requirement 4's repository and branch, and against the fixtures for
requirements 3 and 6.
- A push naming no refspec now resolves an absolute `git -C` from an
unnamed directory, after a `cd "$X"` or an `env -C "$D"`, as the push's
remote already did. Raised in review here, and it was already reachable
through `cd "$X"`.
- `host-setup/agent-safety/README.md` states how an `env` is read, and
that an `env -C` moves the directory requirement 6 tests.

## Local strict review

Three recorded passes at the `frontier` tier, the last over the final
content with no findings. The first pass's findings on an `env -S`
comment, a repeated `-C`, and long-option prefix matching are fixed
here, and the second pass's README wording finding is fixed here too.

Two more recorded passes covered the review fix on the second push. The
first of them caught a signature change that broke `test_install.py`,
which is held by #3704, so the signature was restored.

### Pre-existing findings, filed

- `host-setup/agent-safety/claude/gh-write-guard.py` `_push_targets`,
`pre-existing`: an all-letter word after `-o` is read as a flag cluster,
so `git push -ofoo origin develop` is misread. Filed as #3720.

### Deferred

- `host-setup/agent-safety/claude/gh-write-guard.py`
`_bare_push_branch`, `introduced`, design: its `leading_cd` and `moved`
parameters are now reached only from `test_install.py`. Removing them
needs that file, so it is filed as #3723.

### Dropped findings

- `host-setup/agent-safety/claude/gh-write-guard.py` `_env_option`,
`_skips_hooks`, `_push_targets`, consolidation: one shared short-option
cluster reader. The other two are untouched code with different value
letters, so merging them is a change of its own.
- `scripts/docker_lint.py` `shell_shebang_interpreter`, consolidation:
it also walks `env` options. Declined because the hook installs as one
standalone file and cannot import it.

Closes on promotion: #3716

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Oct 31, 2026
…rite Guard (#3727)

## Summary

Fixes a finding raised on promotion #3715. GNU `env` accepts a unique
abbreviation of a long option, so a constructed `env --ch=/srv/other
GIT_DIR=.git git push origin main` runs git in `/srv/other`. The write
guard read `--ch` as a no-op and resolved the push's repository from the
working directory.

- An `env` option the guard does not recognize, long or short, now
leaves the directory a push runs in unnamed for requirement 4.
`_push_repo` and the bare-push branch reader take `None` in place of the
working directory, the value `env -C "$D"` already produces, so
requirement 4 fails closed only for `main`, `master`, and `develop`, as
#3709 and #3722 treat a shape they cannot resolve. An absolute
`--git-dir`, git's own `-C`, or an absolute `cd` in a wrapper body still
names the repository. A `GIT_DIR` behind such an option is not read for
the repository, since the option may abbreviate `--unset`. Prefix
matching was decided against.
- Each unrecognized option may or may not take the next word, and a
value it takes may be an `-S` string, which reaches an abbreviated
`--split-string`. `_launches` reads every combination of up to four of
those choices. A reading past the first adds commands only and changes
no shell state, and a subshell restores the body's undetermined state
with its directory.
- A push naming no refspec or `HEAD` behind such an option, whose branch
nothing else names, reads it as if the option were absent. So it is
never weaker than before and never newly blocks a feature-branch push.
- `--null`, `--debug`, and the GNU signal options are now listed, so
they keep reading as no-ops. `-0`, `-v`, `-i` and the value letters keep
their behavior.
- Requirement 6 keeps judging the working directory. The extra readings
can only add a deny there, for example `env --chd /path/wt git commit`
run from a primary checkout.
- `host-setup/agent-safety/README.md` states the new reading in its
lexer paragraph, in requirement 4, and in requirement 6.

## Verification

- Reproduced first with `classify` on two constructed scratch checkouts,
origins `alpha/x` and `beta/y`, with a stubbed live-rules lookup
requiring a pull request on `beta/y`'s `main`. Before the fix
`--ch=<b>`, `--chd <b>`, and `-X <b>` pushes to `main` were allowed.
After it they are denied, and feature-branch pushes stay allowed.
- New self-test cases cover `--ch=<dir>`, `--chd <dir>`, an unknown
short letter, two unrecognized options of different arity, an
abbreviated split string, an unknown option ahead of `sh -c`, a subshell
in the body, absolute `-C`/`--git-dir`/body `cd` re-anchoring, a URL
remote, bare and `HEAD` pushes, `GH_REPO` not leaking from a speculative
reading, each recognized option unchanged, and allow on `feature/x`.
Each new repository and branch row fails with its fix reverted.
- `gh-write-guard.py --selftest` passes. Pinned ruff format and check,
mypy, `tests/test_guard_size.py`, and the prose gate pass.
- Seven local strict-review passes, the last raising nothing.

## Dropped findings

- `gh-write-guard.py` `_env_option`, `style`: reading an unknown short
letter as unrecognized was named as over-hardening. The decided fix for
#3725 names short options explicitly.

## Pre-existing findings, not filed

- `gh-write-guard.py` `_bare_push_branch`, `pre-existing`: its
`leading_cd` and `moved` parameters answer no caller, since every caller
passes `None` and `False`. One caller is in
`host-setup/agent-safety/claude/test_install.py`, outside this change.

Closes on promotion: #3725

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Oct 31, 2026
…d Unread Moves Whole, and Name uv's No-Cache Setting (#3715)

Promotes the changes merged into `develop` since promotion #3703 to
`main`, the third round of a backlog burndown.

- [#3709](#3709) Reads a
push's remote, and the branch of a push with no refspec, in the
repository git actually uses in the write guard's owner check
(requirement 4). Before, `--git-dir` and a `GIT_DIR` on the push were
ignored, so a constructed `git --git-dir=<other>/.git push origin
develop` was checked against the working directory's repository rules
and allowed. A shape whose repository the guard cannot determine (an
earlier `GIT_DIR`, a `-c` or `--config-env` override of a `remote.` or
`url.` key, any `GIT_CONFIG*` variable, an unresolvable `--git-dir`) now
blocks `main`, `master` and `develop`. The self-test covers each shape
and `host-setup/agent-safety/README.md` states the requirement.
- [#3712](#3712) Reads a
file moved out of a source some rule never read in full for that rule in
the prose gate: a narrowed directory scope, a `home_path_exempt` file,
instruction text for `issue-ref`, and a move that changes the file's
lexer. Skips YAML block scalars when reading a Compose file's volume
entries.
- [#3707](#3707) Names
uv's no-cache setting, and its remedies, when the repo gate's resolved
PyYAML interpreter is gone at launch, rather than reporting a bare
missing-file error.
- [#3708](#3708) Moves the
section-name lookup key into one `section_key()` in
`spec/fidelity_text.py`, used by the carry, the audit, the skill build
and the review ledger, with no behavior change.

- [#3722](#3722) Applies
`env`'s directory and environment options to the command the write guard
judges, answering this promotion's first review round: `env
-C`/`--chdir` move the repository a relative `GIT_DIR` and the remote
resolve in, `env -i`/`-u` clear a `GIT_DIR` that no longer steers the
check, and `env -S` is read rather than skipped. A run-time directory
reads as undetermined, which blocks `main`, `master` and `develop`.
- [#3721](#3721) Finds a
Compose block-scalar opener through the YAML lexer the gate already uses
rather than a hand-written pattern, so a key holding `#`, quoted or not,
opens the opaque region. Also answers this promotion's first review
round.

- [#3727](#3727) Reads an
`env` option the write guard does not recognize, such as GNU `env`'s
`--ch` abbreviation of `--chdir`, as leaving the push's repository
undetermined, so `main`, `master` and `develop` fail closed, answering
this promotion's second review round. A feature-branch push is never
newly blocked.

- [#3731](#3731) Removes
the fallback #3727 kept for a push naming no refspec behind an
unrecognized `env` option, answering this promotion's third review
round. Such a push resolves its branch only through an absolute
`--git-dir`, git's own `-C`, or a `cd` in a wrapper body, and is
otherwise read as writing every protected default, so it fails closed
even from a feature branch.

Deferred review findings: #3705 (`scripts/README.md`'s uvx promise),
#3706 (`validate.py`'s exact section-name match), #3710 (whether
`section_key` should casefold), #3711 (Compose-name and directory moves
in the prose gate), #3713 (the review reader's `open review findings:`
spelling), #3714 (an edited shebang that changes a file's lexer), #3720
(the write guard's reading of `git push -o<value>`), #3723 (unused
`_bare_push_branch` parameters), #3729 (a bare push after a branch
switch earlier in the same command), #3732 (the review reader's `Review
findings remain:` spelling).

For the maintainer: #3730 asks whether every bare push whose repository
the guard cannot determine should fail closed, as #3731 now does behind
an unrecognized `env` option.

Fixes #3695
Fixes #3684
Fixes #3694
Fixes #2358
Fixes #3686
Fixes #3716
Fixes #3717
Fixes #3725
Fixes #3728

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants