Repository navigation
Read the gh Write Targets the Write Guard Still Missed - #3162
Conversation
The write guard's cross-owner check now reads the targets gh itself reads beyond a repo flag: - a GH_REPO the session's environment holds, a bare `export GH_REPO`, and a plain assignment once GH_REPO is exported - a GH_REPO for each wrapper body it reaches, and inside a quoted substitution - gh api's older `:owner` and `:repo` placeholders, and a `[HOST/]OWNER/REPO` value by its middle owner - a pull request or issue URL argument, a `gh repo` write's repository, a transfer destination, and `issue develop --branch-repo` - write subcommands the table missed, workflow, run, cache, label clone, and others - gh api write paths naming an owner outside `repos/` - a gh write behind a run-time command word, and behind a run-time word before gh's command A run-time owner in any of these fails closed. Every reading only adds a target, so no command develop denies is allowed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local review of the gh-target reading found gaps in the new paths: - the shell's GH_REPO state now reaches a gh word behind a launcher or `find -exec`, and a prefix reaches it behind a launcher - a session-environment GH_REPO is kept beside a later export, since a subshell or function export does not replace it - a quoted substitution takes the state at the word opening it - a repository value is read in gh's ssh and scp URL forms - rule 2 reads a GraphQL mutation behind a run-time command word Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second local review found two over-denies the previous fix added. An environment GH_REPO is now replaced by a top-level, unconditional export or assignment, and by a wrapper's prefix, while one in a subshell, a function, or after `&&` still leaves it read. A gh word that is only an argument, as after `echo`, again takes no GH_REPO. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The third local review found that the top-level test let a backgrounded or piped export, or one past a stray closing token or a case, replace an environment GH_REPO. An export now replaces it only when nothing before it opens or closes a subshell, group, function, or case, it follows the start, `;`, or a newline, and it ends at one of those, `&&`, or `||`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The receipt pass found that the in-shell test still let an export inside an if or loop body, or past a quoted separator, replace an environment GH_REPO, and that its rescans made long commands quadratic. A text export can sit where it never runs, so none replaces the environment value now. Only a prefix does, on the gh word or on its wrapper. That denies `export GH_REPO=<origin repo>; gh ...` in a session whose environment holds a foreign GH_REPO, a deliberate over-deny. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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 |
|
A recorded local strict-review pass covers head |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #3162 +/- ##
==========================================
Coverage ? 63.97%
==========================================
Files ? 17
Lines ? 9151
Branches ? 0
==========================================
Hits ? 5854
Misses ? 3297
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
Moderate guard issues and documentation clarity findings remain unresolved.
1 open finding
What changed in this PR
This PR expands the gh write guard’s repository-target detection and documents the new behavior.
Changes:
- Adds broader target, API, command, and subcommand parsing.
- Handles inherited and wrapper-scoped
GH_REPO. - Expands documentation and self-test coverage.
| File | Summary |
|---|---|
host-setup/agent-safety/README.md |
Documents expanded target detection; 3 nit findings remain. |
host-setup/agent-safety/claude/README.md |
Documents inherited GH_REPO behavior. |
host-setup/agent-safety/claude/gh-write-guard.py |
Implements parsing and tests; 4 moderate findings remain. |
🧠 Review effort: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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 |
… Dash-Name Refusal to Main (#3164) - #3162 makes `gh-write-guard` read the gh write targets it still missed: an inherited `GH_REPO` from the session environment, a run-time word before gh's command, a gh write behind a run-time command word, a positional repository and the missing write subcommands, `gh api` write targets outside `repos/`, and the `GH_REPO` gaps #3135 introduced. It adds no new allow. - #3156 makes both bootstrap loaders reject a dash-prefixed `-Agents`/`--agents` name before the handoff, so `-All` can no longer bind install-agents' switch. Verified natively under pwsh 7 and Windows PowerShell 5.1 in #3161. - #3153 lets a manual dispatch bypass the smoke build's paths filter, in the catalog snippet and the reusable-workflows example together. - #3154 tests the Docker task workflow's credential check, login, and hook inputs. - #3150 bumps the actions-deps group. After this merges, the host-safety installer payload changes, so the maintainer runs the installer after promotion. Closes #3129 Closes #3131 Closes #3132 Closes #3133 Closes #3136 Closes #3147 Closes #3148 Closes #3149 🤖 Generated with [Claude Code](https://claude.com/claude-code)

Summary
The write guard's cross-owner check (rule 3) read a write's target only from a repo flag, a
repos/API path, and aGH_REPOset in the command text. gh reads its target from more places than that. This change reads each of them, building on the command and subcommand reader #3135 added. Every new reading only adds a target, so nothing develop denies is allowed.GH_REPO(Read an Inherited GH_REPO From the Session Environment in the Write Guard #3147). The value in the environment the session was launched with now reaches every gh word, wrapper bodies and substitutions included. A command-text prefix or a repo flag still overrides it, as gh reads it. The hook reads the same environmentGH_WRITE_GUARD_ALLOWis read from. A value a Bash call gets another way, such as from a shell profile, is not seen, and the README says so.gh "$R" pr comment 5is read as a comment whose run-time word may be a repo flag, so it fails closed, asgh pr "$R" commentdoes.GH_REPO=x; export GH_REPOreaches gh. So does a plain assignment once GH_REPO is exported, and anexportargument past a bare name.$G,"$G",${G}, and"$(command -v gh)"before a literal gh write are read as gh, for rules 1, 2, 3, and 5.gh repowrite's repository argument,gh issue transfer's destination, andgh issue develop --branch-repoare read too, in gh's[HOST/]OWNER/REPO, https, ssh, and scp forms. The table gainspr revert/update-branch,issue develop,release delete-asset,repo unarchive/sync,label clone,workflow run/enable/disable,run rerun/cancel/delete,cache delete,repo deploy-key add/delete, andrepo autolink create/delete.gh apiwrite targets outsiderepos/(Read a gh api Write Target Outside a repos/ Path in the Write Guard #3132).orgs/<owner>,users/<owner>,user/starred/<owner>/<repo>,user/following|blocks/<user>, anduser/memberships/orgs/<org>name an owner. An api write whose endpoint opens with a run-time word, or whose owner segment is one, fails closed.GH_REPO(Read a GH_REPO Only in the Wrapper Body It Reaches in the Write Guard #3144, under-deny side). Each wrapper's own reaching value is now read for its own bodies, nested ones inheriting. So is a quoted"$(...)"substitution's. Develop allowedGH_REPO=stranger/example bash -c 'gh issue comment 5 -b hi'; GH_REPO=example/repo bash -c 'gh pr view 5'. The old latest-value reading is kept beside it, so the issue's own over-deny example stays denied.gh api's older:owner/:repoplaceholders takeGH_REPO(confirmed live: gh 2.102 still fills them). A three-part value is read by its middle owner as well, which closes--repo example/stranger/x. The first-part reading is kept, soGH_REPO=github.com/example/repostays denied.The maintainer's rule on #3079 holds throughout: a run-time repository value fails closed only when its owner is run-time, so
gh repo create "example/$NAME"andorgs/example/$Tare compared by their literal owner.Scope Choices
GH_REPOis replaced only by a prefix, on the gh word or on its wrapper. A command-text export can sit where it never runs, in a subshell, an untaken branch, a function, or a backgrounded or piped command, so the environment value is read beside every export. That deniesexport GH_REPO=example/repo; gh pr comment 5in a session whose environment holds a foreignGH_REPO, a deliberate over-deny. A prefix,GH_REPO=example/repo gh pr comment 5, or-Ris the allowed spelling, and the Claude README now says so.gh pr comment "$PR", is left allowed. Whether it should fail closed is Decide Whether a Run-Time Pull Request Selector Fails Closed in the Write Guard #3157.Develop-Versus-Branch Differential
The corpus is every command in develop's self-test tables (1255 commands) plus 165 constructed commands, each tagged with the issue it exercises or as a control. Each was run under both guards with origin
example/repoand again with a second owner, 2840 runs.example/...is itself a foreign owner, so each is the same shape as its issue's case: one Read a Run-Time Word Before gh's Command and a Bare GH_REPO export in the Write Guard #3136, one Read a gh Write Behind a Run-Time Command Word in the Write Guard's Target Check #3131, six Read a Positional Repository and the Missing Write Subcommands in the Write Guard #3133, and three Read a gh api Write Target Outside a repos/ Path in the Write Guard #3132.classify. An exception there would let the hook fail open.Tests and Revert Proof
_GH_TARGET_CASESadds 82 self-tests, run with the environment each names, originexample/repo. They cover every closed gap and a control for each.--selftestpasses with 1413 cases.python3 -m unittest discover -s testspasses 2873 tests, andtest_install.pypasses 162.prose_lint.py --diff origin/develop,repo_gate.py --check eolandeol-coverage,spec/validate.py, markdownlint, editorconfig-checker, and cspell are all clean.Local Review
Five full passes at
frontier, plus a receipt pass over the last README sentence.Round one raised 8 findings. These were fixed:
find -exec$G api graphqlRounds two through four were all about one question, when a command-text export may replace an environment
GH_REPO. Each test that let one replace it was missing a case: a subshell, a backgrounded or piped export, acase, an if or loop body, or a quoted separator. The last test was also quadratic on long commands. That logic is deleted rather than patched again, so only a prefix replaces the value, per the scope choice above. A 26k-character command classifies in 0.46s, as on develop. Round two also found a gh word that is only anechoargument takingGH_REPO, and that is fixed. Round five found no code defect and a README gap, which is filled.These were filed:
--org/--ownerflags, issue-reference flag values,repositories/<id>paths,evalpayloads, dot-segments, anddeclare -x, as Read the Owner Flags, Issue References, and Repository Ids the Write Guard Still Skips #3158These were declined:
_export_wordsinto the git-side export reader, which would change git-rule verdicts outside these issuessentence-lengthcheck, which has about 175 existing hits in these filesCloses #3147
Closes #3136
Closes #3131
Closes #3133
Closes #3132
Refs #3144, whose over-deny example waits for the over-denial lane
Refs #3145, whose item 3 over-deny (
GH_REPO=github.com/example/repodenied) waits for the same lane🤖 Generated with Claude Code