Repository navigation
Read a Push's Remote and Branch in the Repository Git Uses in the Write Guard - #3709
Conversation
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>
|
A recorded local strict-review pass covers head |
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
There was a problem hiding this comment.
🟡 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-diris treated as unresolved after an unknowncdor-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 Report✅ All modified and coverable lines are covered by tests. 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
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:
|
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>
|
A recorded local strict-review pass covers head |
|
Copilot's overview on e23b05b listed two findings under |
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>
…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>
…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)

Summary
#3695 asked whether the write guard's requirement 4 reads a push's remote in the repository a
--git-dirnames. It did not. A check with two constructed checkouts,awith originhub.lumenfield.work/alpha/xandbwith originhub.lumenfield.work/beta/y, run froma, showed the guard queryingalpha/xfor each of these:git --git-dir=<b>/.git push origin :feature/x, and the same with a separate--git-dirvaluegit --git-dir=<b>/.git push origin develop, wherebeta/yrequired a pull request ondevelopand the guard allowed the pushGIT_DIR=<b>/.git git push ...,env GIT_DIR=... git push ..., andexport GIT_DIR=...; git push ...-C <b>and-C <b> --git-dir=.gitalready readbeta/y, and--work-tree=<b>readsalpha/x, as git itself does._push_reponow reads the remote's push URL in the repository git uses: the one a--git-dir, or else aGIT_DIRthe push itself carries (as a prefix or throughenv), names, resolved from the directory after-C.GIT_DIRset earlier in the text reads as undetermined, since whether git sees it turns on an export or anunsetthe reading does not track.-cor--config-envoverride of aremote.orurl.key, aGIT_CONFIGvariable in the text, and a--git-dirthe reading cannot resolve each leave the repository undetermined. That fails closed formain,master, anddevelop, as the spec already states for an unresolvable origin.--git-diror the push's ownGIT_DIRnames, an absolute--git-direven after an unresolvedcd. It was left unjudged before, so such a push into adevelopcheckout passed. One whose repository stays undetermined is still allowed, as the README now states.host-setup/agent-safety/README.mdrequirement 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 caseok.GIT_DIRtrusted, and an absolute--git-dirafter an unresolvedcd), fixed in the third. The last local pass, over the full diff at the head, raised none.tests/test_guard_size.py, and the prose gate on the diff: clean.Closes on promotion: #3695
🤖 Generated with Claude Code