Skip to content

Read the gh Write Targets the Write Guard Still Missed - #3162

Merged
ptr727 merged 8 commits into
developfrom
feature/guard-gh-target-followups
Oct 16, 2026
Merged

ptr727 merged 8 commits into
developfrom
feature/guard-gh-target-followups

Conversation

@ptr727

@ptr727 ptr727 commented Oct 16, 2026

Copy link
Copy Markdown
Owner

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 a GH_REPO set 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.

  • An inherited 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 environment GH_WRITE_GUARD_ALLOW is read from. A value a Bash call gets another way, such as from a shell profile, is not seen, and the README says so.
  • A run-time word before gh's command, and a bare export (Read a Run-Time Word Before gh's Command and a Bare GH_REPO export in the Write Guard #3136). gh "$R" pr comment 5 is read as a comment whose run-time word may be a repo flag, so it fails closed, as gh pr "$R" comment does. GH_REPO=x; export GH_REPO reaches gh. So does a plain assignment once GH_REPO is exported, and an export argument past a bare name.
  • A gh write behind a run-time command word (Read a gh Write Behind a Run-Time Command Word in the Write Guard's Target Check #3131). $G, "$G", ${G}, and "$(command -v gh)" before a literal gh write are read as gh, for rules 1, 2, 3, and 5.
  • Positional repositories and missing write subcommands (Read a Positional Repository and the Missing Write Subcommands in the Write Guard #3133). A pull request or issue URL argument names its repository. A gh repo write's repository argument, gh issue transfer's destination, and gh issue develop --branch-repo are read too, in gh's [HOST/]OWNER/REPO, https, ssh, and scp forms. The table gains pr 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, and repo autolink create/delete.
  • gh api write targets outside repos/ (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>, and user/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.
  • Wrapper-scoped 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 allowed GH_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.
  • Close Three Small GH_REPO and Merge-Message Gaps in the Write Guard #3145. The merge deny message now names a quoted run-time word where gh itself goes. gh api's older :owner/:repo placeholders take GH_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, so GH_REPO=github.com/example/repo stays 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" and orgs/example/$T are compared by their literal owner.

Scope Choices

  • An environment GH_REPO is 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 denies export GH_REPO=example/repo; gh pr comment 5 in a session whose environment holds a foreign GH_REPO, a deliberate over-deny. A prefix, GH_REPO=example/repo gh pr comment 5, or -R is the allowed spelling, and the Claude README now says so.
  • A wholly run-time pull request or issue selector, as in 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/repo and again with a second owner, 2840 runs.

Tests and Revert Proof

  • _GH_TARGET_CASES adds 82 self-tests, run with the environment each names, origin example/repo. They cover every closed gap and a control for each.
  • Revert proof: the 82 cases were run against develop's guard, that is, with the fix reverted. All 56 deny cases fail there and all 26 allow controls pass, so each test fails without its fix.
  • --selftest passes with 1413 cases. python3 -m unittest discover -s tests passes 2873 tests, and test_install.py passes 162.
  • ruff check and format, mypy, prose_lint.py --diff origin/develop, repo_gate.py --check eol and eol-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:

  • the shell state now reaches a gh word behind a launcher or find -exec
  • a substitution takes the state at the word opening it
  • ssh and scp repository forms are read
  • rule 2 now covers $G api graphql

Rounds 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, a case, 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 an echo argument taking GH_REPO, and that is fixed. Round five found no code defect and a README gap, which is filled.

These were filed:

These were declined:

  • merging _export_words into the git-side export reader, which would change git-rule verdicts outside these issues
  • the ungated sentence-length check, which has about 175 existing hits in these files
  • a Windows env-name case item, which is unlikely

Closes #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/repo denied) waits for the same lane

🤖 Generated with Claude Code

ptr727 and others added 6 commits October 16, 2026 11:28
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>
Copilot AI lite review requested due to automatic review settings October 16, 2026 19:43
@coderabbitai

coderabbitai Bot commented Oct 16, 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: b446f98b-ffe0-4daf-ac99-d6cdaab8a7b8
  • 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.

@ptr727

ptr727 commented Oct 16, 2026

Copy link
Copy Markdown
Owner Author

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

@codecov

codecov Bot commented Oct 16, 2026

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff             @@
##             develop    #3162   +/-   ##
==========================================
  Coverage           ?   63.97%           
==========================================
  Files              ?       17           
  Lines              ?     9151           
  Branches           ?        0           
==========================================
  Hits               ?     5854           
  Misses             ?     3297           
  Partials           ?        0           
Flag Coverage Δ
python-3.13 63.97% <ø> (?)

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

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.

Comment thread host-setup/agent-safety/README.md Outdated
ptr727 and others added 2 commits October 16, 2026 12:49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ptr727

ptr727 commented Oct 16, 2026

Copy link
Copy Markdown
Owner Author

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

@ptr727
ptr727 merged commit a152f47 into develop Oct 16, 2026
13 checks passed
@ptr727
ptr727 deleted the feature/guard-gh-target-followups branch October 16, 2026 20:03
ptr727 added a commit that referenced this pull request Oct 16, 2026
… 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)
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