Repository navigation
Read a gh Write's Subcommand and Target the Way gh Parses Them - #3135
Conversation
The write guard found a gh write by matching `gh <command> <subcommand>` as adjacent text, so a repo flag before the command or between `pr` and its subcommand hid the write from the cross-owner check. It read only `-R`, `--repo`, and their attached forms as a target, so a short-option cluster ending in `R` and a `GH_REPO` assignment went unread, and a run-time repository value gave the check nothing to compare. The guard now finds the command and subcommand the way cobra does, past each flag before them, and reads the target from a cluster's `R` and from a `GH_REPO` assignment too. A gh write whose repository is a run-time word fails closed. A run-time word where `pr` or `merge` goes, or a run-time command word before `pr merge`, fails closed beside `--admin` or where word splitting may split it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local review found the first reading too broad. A run-time repository name under a literal origin owner was denied, though the owner is all the check compares. A GH_REPO word after an env precommand was read as an assignment, a GH_REPO prefixing a read reached a later write, and heredoc data was read as commands. The run-time check now reads only the owner, GH_REPO is read through the prefix and export primitives the git readings use, scoped to the gh word it prefixes, and both readings leave heredoc data out. The subcommand predicates share one helper, and the README states the fail-closed exception to rule 3's precision stance. Two comment blocks move back above the code they describe, as they sit on develop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second local review found an export read as reaching every write in the command, including one before it, and a command substitution such as `$(which gh)` missed as the command word before `pr merge --admin`. An export now reaches only the gh words after it, and a wrapper body is read with the exports of the command around it. The run-time command word is read past its whole substitution span, as the git reading does. The run-time target reading shares the wrapper walk through `_all_gh_arg_lists`. 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 |
The third local review found `gh $REPO_ARGS pr merge 5 $MERGE_FLAGS` allowed where develop denied it. The cobra reading took the run-time word as gh's command, so the argv read as neither a merge nor a possible one. It also found a repo flag not overriding GH_REPO, a wrapper's own GH_REPO prefix unread, and wrapper bodies inside heredoc data read. A run-time word read as gh's command or subcommand before a `merge` after `pr` now makes the argv a possible merge. Only a word in exactly the `pr` or `merge` slot is read as the one word it is when quoted, since one elsewhere may be a repo flag. GH_REPO is skipped where the argv names a repo flag, a wrapper takes what `_wrapper_env_assignments` says reaches it, and both readings leave heredoc data out, wrapper bodies included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #3135 +/- ##
==========================================
Coverage ? 63.94%
==========================================
Files ? 17
Lines ? 9151
Branches ? 0
==========================================
Hits ? 5852
Misses ? 3299
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
Unresolved guard bypasses must be fixed before approval.
5 open findings
Parse API subcommands after global flags · New Handle runtime flags before merge commands fail-closed · New Fail closed on runtime targets without a git origin · New Document denial of additional runtime words in may-merge argv · New Document runtime-word denial after quoted may-merge subcommands · New
What changed in this PR
This PR improves gh write detection by following Cobra-style command parsing and recognizing repository targets, GH_REPO, and runtime arguments.
Changes:
- Adds parser-aware command, subcommand, and option-cluster handling.
- Adds repository-target and runtime-value checks.
- Updates documentation and self-tests.
Unresolved findings include guard bypasses involving api global flags, runtime flags before pr, and runtime targets without an origin; explicit repository-flag precedence also needs correction. Documentation should clarify may-merge runtime-word behavior.
| File | Summary |
|---|---|
host-setup/agent-safety/README.md |
Documents expanded parsing; needs clarification of may-merge runtime-word denial. |
host-setup/agent-safety/claude/README.md |
Updates guard documentation; needs the same runtime-word rule documented. |
host-setup/agent-safety/claude/gh-write-guard.py |
Implements parsing and target checks, but retains critical/P1 bypasses and a moderate precedence issue. |
🧠 Review effort: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot found `gh --hostname github.com api -X POST repos/...` read as
no write, since the api readers took `api` only as the first word and
cobra passes the flags before a command to it. A flag-preceded `api` now
moves to the front of the argv the readers see.
The fourth local review found a quoted run-time word two words before
`merge` read as the merge slot, an unquoted run-time command word not
read as holding `pr merge --admin`, an attached value such as
`-BRelease/2.0` read as a repo flag, and GH_REPO read on a gh api path
written out. The merge slot now needs the word beside `pr` or `merge`,
an unquoted run-time word where gh's command or subcommand goes may be a
merge, a cluster is read by the value options its own subcommand
defines, and gh api takes GH_REPO only for an `{owner}` or `{repo}`
placeholder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fifth local review found a common read denied: `gh api repos/example/repo/pulls/$N` read its endpoint as gh's subcommand slot, so the unquoted run-time word in it read as a possible `pr merge`. Only a run-time command word, or a run-time word after a literal `pr`, may be a merge, as the README states. The GH_REPO reading also takes the argv with a flag-preceded `api` moved to the front, as the other readers do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
The sixth local review found no case pinning the four shapes develop denied and gh refuses, which a later change could undo silently. They are now self-test cases. It also found the `_is_gh_write` docstring silent on a possible write through a run-time word, the short-option table's docstring claiming every write subcommand, an unwrapped docstring line, and the merge deny message silent on a run-time word where gh itself goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
… Fixes to Main (#3146) - #3135 makes `gh-write-guard` read a gh write's subcommand and target the way gh parses them: the subcommand is found past flags before the command and before the subcommand, an `R` inside a short-option cluster is read as a target, `GH_REPO` is read as a target, a run-time repository owner on a write fails closed, and a run-time word where `pr` or `merge` goes fails closed beside `--admin` or where word splitting may split it - #3139 releases the bootstrap lock when a `bootstrap.ps1` run ends rather than when its process does, so a second run typed at the same pwsh prompt is no longer refused the lock - #3130 matches tool names and the `all` keyword by case in the Windows agents loaders, as the Linux loaders already do - #3128 reads CodeRabbit's rate limit on a reply to an explicit review command in `pr_review.py` - #3126 reconciles VSCode-Server-DotNetCore in the hub after its resync - #3124 logs the Docker task's build-base leg in to Docker Hub - #3122 names the carried skill and the skipped review correctly - #3120 states the subprocess encoding rule in `CODESTYLE.md`, where carriers read Python style - #3118 and #3111 move the release-guard tests' workflow-command checks onto `runner_commands`, and test the alternatives wiring at the `audit_repo` and `fidelity_pass` call sites - #3116 stops the doc gates' printed names forging workflow commands - #3113 adds a catalog snippet for the release-with-smoke caller - #3109 moves the `workflow_reuse` fetch onto `audit.contents_text` - #3105 accepts `.codecov.yml` as an alternative to `codecov.yml` - #3103 adds a pip ecosystem to the carried Dependabot snippet - #3100 prefers the venv's ruff and pyright for a declared pip directory - #3098 tracks composite action pins in Dependabot - #3096 reads an uppercase Markdown suffix as Markdown at every prose gate branch After this merges, the host-safety installer payload changes, so the maintainer runs the installer after promotion. Closes #1416 Closes #1425 Closes #1578 Closes #1874 Closes #1892 Closes #1899 Closes #2804 Closes #2850 Closes #2852 Closes #2854 Closes #2996 Closes #3065 Closes #3067 Closes #3069 Closes #3079 Closes #3080 Closes #3089 Closes #3093 Closes #3101 Closes #3106 Closes #3107 Closes #3137 🤖 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 pull-request smoke build that runs when relevant release files change and reports the combined validation and build status. * Repository audits now recognize `.codecov.yml` as an alternative to `codecov.yml`. * **Improvements** * Python linting and type checks use tools from declared project environments when available, with fallbacks for missing tools. * Workflow and lint reports safely display unusual filenames and command-like text. * GitHub write-target checks now account for additional command forms and fail closed when targets cannot be verified. * Rate-limit detection now recognizes another CodeRabbit response format. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## 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` (#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 (#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 (#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 (#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/` (#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` (#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. - **#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 #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. - **Flips:** 189, every one allow to deny, and **new allows: 0**. - **Every flip has an issue:** #3147 (38), #3136 (27), #3131 (23), #3133 (64), #3132 (27), #3145 (7), #3144 (3). Those counts include the 11 flips tagged control. All 11 are under the second owner, where `example/...` is itself a foreign owner, so each is the same shape as its issue's case: one #3136, one #3131, six #3133, and three #3132. - **Unexplained flips:** none. No case from develop's own tables flipped. - **Fuzzing:** 42,000 random constructed commands across the branch's heads, 6,000 of them on the final one, plus about 100,000 to 160,000 per pass from the local reviewer. No new allow, and no exception from `classify`. An exception there would let the hook fail open. ## 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: - the run-time selector, as #3157 - `--org`/`--owner` flags, issue-reference flag values, `repositories/<id>` paths, `eval` payloads, dot-segments, and `declare -x`, as #3158 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](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… 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 found a gh write by matching
gh <command> <subcommand>as adjacent text. It read a target only from-R,--repo, and their attached forms. That left six holes in one class, each a gh write whose target or subcommand it misread. This change gives the guard one reading in place of six local patches._gh_path). The reading goes past each flag before the command and before the subcommand, and an unknown flag takes the next word as its value. A write is then looked up in one table,_GH_WRITE_SUBS, which the old text pattern is now built from. This closes the misses where a repo flag sits before the command (Read a Repo Flag Placed Before the gh Subcommand in the Write Guard's Target Check #3093) or betweenprand its subcommand (Read a gh Write Subcommand Past Flags Between pr and It in the Write Guard #3080). It also readsgh pr --squash 5 merge "$(...)"as the merge it is, andgh --hostname github.com api -X POST ...as thegh apicall it is.Rin a short-option cluster is read as a target, as pflag reads-dR xas-d -R x(Read a gh Short-Option Cluster Ending in -R as a Target in the Write Guard #3067). Each cluster is read by the value options its own subcommand defines, so-BRelease/2.0stays a base branch.GH_REPOis a target (Read GH_REPO as a gh Write's Target in the Write Guard #3089). It is read through the_command_prefix,_export_assignments, and_wrapper_env_assignmentsprimitives the git readings use. A prefix reaches only the gh word it prefixes, anexportreaches only the gh words after it, and a repo flag on the argv overrides it, as gh reads it.--repo "$T",GH_REPO="$T", andrepos/$O/.... A literal owner, as in--repo "example/$R", is compared as usual, since the owner is all rule 3 compares. A read naming a run-time repository, and heredoc data, are left alone.prormergegoes may be a merge (Fail Closed on a Run-Time gh Command or Subcommand Word Beside --admin in the Write Guard #3069). It fails closed beside a literal--admin, or where word splitting may split it, as ingh pr $S 5. So does a run-time command word beforepr merge, as inG=gh; $G pr mergeor$(which gh) pr merge, and a run-time word read beforepr merge, as ingh $REPO_ARGS pr merge 5. A quoted single word in exactly theprormergeslot, with no--admin, is read as the one word it is.gh's reading was checked live with read-only calls on gh 2.102. An unknown flag before a subcommand takes the next word, as
gh pr --json number view 1runsview.--helpis the only boolean inherited at theprlevel.--ends the subcommand search.Differential
develop's guard and this branch's
classifywere run over 1,821 constructed commands. Origin wasexample/repo, with empty branch rules, no aliases, and no config. The commands cover gh writes and reads, includingpr merge,create,comment,edit,view,list,diff,checks,issuewrites and reads,release,label, andapireads and mutations. Each carries repo flags in every position and spelling, clusters,GH_REPOprefixes, exports,envforms, run-time words in every position, heredocs, and wrappers. 461 verdicts flip, every one explained below.gh -R stranger/example pr merge 5 --squashgh pr -R stranger/example merge 5 --squashgh pr merge 5 --squash -dR stranger/exampleGH_REPO=stranger/example gh pr merge 5 --squashgh pr create --title t --body b --repo "$T"gh pr $S 5 --admingh --foo pr merge 5 "$X"The #3093 row counts one wrapper case and two
apiwrites whose flags precedeapi. One case in the #3089 row is a known over-deny, listed under Remaining Findings below.Two reads flip to deny, each the #3069 shape. In
for s in view diff; do gh pr $s 5; donethe unquoted$swhere the subcommand goes may split intomerge --admin. Ingh pr "$s" "$PR"the$PRbeside a possible merge may be--admin. The deny says how to write each out.gh pr "$s" 5and every common read with a run-time number, such asgh api repos/example/repo/pulls/$Nandgh pr view $PR, stay allowed.The four deny-to-allow flips are each a command gh itself refuses. develop read them as merges because it took the first word after
pr's flags as the subcommand.gh --foo pr merge 5 "$X":--footakespras its value, so gh fails with an unknown command,merge.gh pr --squash merge 5 "$X"andgh pr -d merge 5 "$X": the flag takesmergeas its value, so5is an unknown command.gh pr -- merge 5 "$X":--ends the search, so gh fails with an unknown command.Tests
_GH_PATH_CASESadds 55classifycases, originexample/repo, 32 expecting a deny and 23 an allow.GH_REPOscoping, heredoc data, the merge slot, cluster value letters, and common reads with an unquoted run-time endpoint word. Some are denies develop already gave that an earlier branch commit lost, such asgh $REPO_ARGS pr merge 5 $MERGE_FLAGS.Gates run locally on the final commit: ruff check and format, mypy, the unittest suite,
spec/audit.py --selftest,spec/workflow_reuse.py --selftest,gh-write-guard.py --selftest,test_install.py,build_dist.py --check,canonical_review.py status,repo_gate.py, both prose invocations and the diff-scoped one, the jq sweep,spec/validate.py, anddocker_lint.py.Local Review
Seven frontier passes ran, each recorded: three before the opening push, three before the first fix push, and one before the second. Every
introducedfinding is fixed, declined, or filed below. Declined with reasons:Rand-dR=forms, flagged as over-hardening. Read a gh Short-Option Cluster Ending in -R as a Target in the Write Guard #3067 lists-sdRstranger/exampleitself.GH_REPOon a read compared across the whole command. This matches develop's reading of a literal-Ron a read._is_gh_writeforgh apihandling._is_gh_writestill reaches it, through_gh_argv_is_write.$token. It costs about a second at 1,500 variables, far past any agent command.gh pr "$S" 5 2>/dev/nulldoes. Reading a possible write as a write is the design, and the_is_gh_writedocstring states it.Filed as
introducedrather than fixed here, each a low-realism edge of a new reading:GH_REPOreaching one wrapper is read in every wrapper body, sobash -c 'gh issue comment 5 --body hi'; GH_REPO=stranger/example bash -c 'gh pr view 5'is denied.gh api's older:ownerplaceholder is unread, and a[HOST/]OWNER/REPOvalue is split at its first slash, as develop already does for--repo.Filed as gaps develop already has: #3131, #3132, #3133, #3134, #3136. The new cluster reader is noted on #3071 as a site for its shared primitive.
Closes #3093
Closes #3080
Closes #3067
Closes #3089
Closes #3079
Closes #3069
🤖 Generated with Claude Code