Repository navigation
Read Config and Alias Rewrites the Write Guard Missed - #3306
Conversation
Close seven gaps where the guard read git config or an alias other than the way git does, each by adding a denial or failing closed: - A push after a git config, a git remote, a subcommand that may be an alias, a gh command, or a wrapper shell running one reads its remote's mirror key and its checkout's origin as unresolved (#3046). - A persisted alias read that times out or cannot run is unresolved rather than no alias, in both alias walks (#3047). - A core.hooksPath override is denied on any git command, except a /dev/null value on a command that is not a commit, a push, or a name that may be an alias (#3055). - A HOME or XDG_CONFIG_HOME other than /dev/null on a git command is read as an unread config file (#3057). - Environment config is paired per git command as well as across the reading, so a later command reusing an index cannot hide a key (#3072). - Rule 6 reads a persisted alias from the repository git runs in, and a wrapper shell's GIT_DIR or GIT_WORK_TREE reaches its body (#3078). - An alias expansion is read past its leading global options, and a -c alias.<name> among them is followed (#3081). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Leave a git config or git remote that only reads out of the commands that may rewrite a push's remote config, such as --get, --list, or remote -v. - Read an alias expansion's core.hooksPath through the same reason a command line's is read, so a /dev/null value on status is allowed. - Share one per-shell reader between the wrapper environment and the wrapper directory reads, and one predicate for a word that may be an alias. - Drop the count of rule 6's fail-closed places, which the timed-out alias read made stale, and state the new denials in the kit README and the module docstring. - Split the sentences the sentence-length check flagged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Read a git config or git remote in a backtick substitution before a push, which the tokens split, and read a config or remote whose arguments hold a substitution as one that may write. - Count a shell with no command string the guard reads, such as a script file or a pipe, before a push. - Count a gh only where it is its simple command's command, so a path or an argument named gh does not run it. - Pin each arm of the config-change scan with a selftest row, and say in the spec that an alias's core.hooksPath is judged as on a command line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
Answer the findings the pull request body lists from the third local review pass: - Read substitution bodies before a push with their quoting respected, so a backtick inside single quotes is literal text. - Read a lone git config key, a get or list subcommand, and options that take a value as reading, and a write action as writing. - Count a wrapper shell whose command string starts with a run-time word, and a shell only where it runs a script file or its input. - Count a git or gh word only as its simple command's command, and a gh only where its command group can touch git config. - Rebind the README sentence the last change misattached, and name the shell and substitution forms in both READMEs. 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 #3306 +/- ##
========================================
Coverage 65.49% 65.49%
========================================
Files 17 17
Lines 10011 10011
========================================
Hits 6557 6557
Misses 3454 3454
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:
|
|
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 |
The previous commit narrowed the scan with hand-rolled readers, and a local review pass found each one opened an allow the commit before it denied. Each now reuses the helper that already answers its question: - A word runs as a command where _runs_as_command says so, a launcher such as flock or watch included, or right after an opening paren, unless it is an argument of an earlier git word. - A shell with no command string, read by _command_string_index, runs a script file or its input, unless given only --version or --help. - A gh command group is read with _gh_path, past a repository flag. - A run-time command word counts, at the top level and in a body. - A git config write action is read whole or cut short, as _spells_long_option reads one. - Substitutions are read only up to the last push, since one after every push cannot run before it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Answer the local review pass over the previous commit: - Count a run-time word only where it is its simple command's command or follows an opening paren, so an argument such as mkdir -p "$D" runs nothing. - Read substitutions after the last push where a later command may run first, as the push scan reads a background push, a pipe, a loop, or a function. - Say in the spec that any gh group outside the read list counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved guard-analysis issues can cause incorrect denials or missed safety checks.
8 open issues
Distinguish option-only shells from unread scripts · New Scope wrapper directory state to each body · New Preserve quoting when detecting substitutions · New Require command-position validation for git detection · New Treat single-key git config queries as read-only · New Clarify alias denial conditions · New Document guarded shell substitution forms · New Share one ordered invocation walker · New
What changed in this PR
This PR hardens the Git write guard against missed configuration, alias, hook-path, environment, and wrapper-analysis cases.
Changes:
- Adds fail-closed config and alias resolution.
- Expands remote, hook-path, environment, and wrapper detection.
- Updates safety documentation and self-tests.
| File | Summary |
|---|---|
host-setup/agent-safety/README.md |
Documents expanded guard behavior. |
host-setup/agent-safety/claude/README.md |
Updates the implementation summary. |
host-setup/agent-safety/claude/gh-write-guard.py |
Implements config, alias, hook, and wrapper analysis. |
Review effort: Lite
|
A recorded local strict-review pass covers head |
|
Answers to the local review findings this pull request's body lists (7 listed at the first push, plus those of the later passes). The round is the local pass recorded on 69e26fe and the passes after it, each attested on this pull request.
The later passes' findings are each Fixed in d7bae25 or 03f0e87, except two groups, as the body lists. The run-time-under-launcher, |
… Fix to Main (#3310) ## Summary Promotes these pull requests from develop. - [#3306](#3306): the write guard reads git config and alias rewrites it previously missed, each fix adding a denial or failing closed. - [#3301](#3301): corrects the smoke stub comment's `enable_*` default claim, since `enable_release_asset` defaults false. Closes #3046 Closes #3047 Closes #3055 Closes #3057 Closes #3072 Closes #3078 Closes #3081 Closes #3294 🤖 Generated with [Claude Code](https://claude.com/claude-code)



Summary
Closes seven allow gaps in the write guard where it read git config or an alias other than the way git does. Each fix adds a denial or fails closed. None relaxes an existing denial.
git configorgit remotethat does not only read, a git subcommand that may be an alias, and aghcommand. A wrapper shell or a substitution running one counts too, and so does a shell whose command string the guard does not read. A push after one of them now reads its remote'smirrorkey and its checkout's origin as unresolved. A checkout or a switch changes neither, and neither does a--get, a--list, or aremote -v._config_aliasreads through_git_readand answers_UNRESOLVEDwhen the read times out or cannot run. Both alias walks fail closed on it where the name may be an alias. Exit 1 (not set) and other failures still read as no alias.core.hooksPathoverride is denied on any git command, whether set by-c, by--config-env, or in the environment. A/dev/nullvalue holds no hook and is allowed, except on a commit, a push, or a subcommand that may be an alias, where it skips the hooks. An empty value is denied, since git then looks for hooks in the filesystem root (traced with strace on git 2.47). Theconfig --listcontrol row's earlier decision no longer stands, so it now expects a deny. A/dev/nullcontrol row replaces it.HOMEorXDG_CONFIG_HOMEother than/dev/nullthat reaches a git command is read as an unread config directory, asGIT_CONFIG_GLOBALalready is. That covers a prefix, an export, and a wrapper shell's prefix. The repository's own scripts and docs hold no shell-levelHOME=... gitshape. The tests that setHOMEdo it in a Python subprocess environment, which the hook never sees.GIT_CONFIG_KEY_<n>/GIT_CONFIG_VALUE_<n>are paired per git command, from the exports before it and its own prefix. The old pairing across the whole reading is kept too, so no value develop read is lost.--work-treedoes not move, and still from the work tree. AGIT_DIRorGIT_WORK_TREEon a wrapper shell's prefix, or exported before it, is carried into the invocations read from its body.-p,-c) in both walks, and a-c alias.<name>=<value>among them is followed. An include or a--config-envamong them is denied. So is acore.hooksPathread as on a command line, and a command key whose program is unreadable or runs git.The local review raised three sibling gaps that develop shares. They are filed rather than widened into this change: #3303 (persisted config written earlier in the same command), #3304 (a direct git command reads fewer
GIT_DIRforms than a wrapped one), and #3305 (an alias carrying--no-verify). The Copilot round's over-denial from applying one wrapper'sGIT_DIRto every wrapper body is filed as #3309.Local Review Findings and Their Outcomes
Each push's two-round local review budget was spent, so these went to this pull request's loop. Every one is
introducedand now has an outcome.Listed at the first push (pass 3 on 69e26fe):
$(inside single quotes read as a live substitution. Fixed in 45f1d00.git config <key>counted as a write. Fixed in 45f1d00.bash -c "$CMD"before a push was not counted. Fixed in 45f1d00 (and in bodies, 03f0e87).makerun directly is the documented "a write inside a script file is unseen" limit.ghbut notgit. Fixed in d7bae25.So isbound to the wrong clause. Fixed in 45f1d00.Raised by the passes on 45f1d00 and d7bae25, each fixed in d7bae25 or 03f0e87:
flockorwatchrun their git word.ghgroups are now read past a repository flag with_gh_path.mkdir -p "$D", runs nothing.ghread such asgh pr vieworgh repo viewno longer counts.Raised by the pass on 03f0e87, and the design points from every pass:
while, anevalinside a body or substitution, a bodilessxargs ... bash -c, and the deny message naming the wrong cause. Deferred to Read the Config-Change Scan's Remaining Run-Time, Eval, and Xargs Forms in the Write Guard #3307. Develop allows each of these, so none relaxes a develop denial._config_may_changeand_head_may_move, one command-position helper, and onecore.hooksPathpredicate. Deferred to Share One Command-Position Walker Across the Write Guard's Readers #3308.The config-change scan for #3046 drew new findings in every local pass and in the Copilot round. That recurring pattern is escalated to the maintainer with a recommendation in the drive's report, rather than answered by another narrowing round.
Closes #3046
Closes #3047
Closes #3055
Closes #3057
Closes #3072
Closes #3078
Closes #3081
Verification
_UNRESOLVEDsentinel through theconfig_lookupseam raiseAttributeErroron develop's walk, which takes only a string. The fourth is a control that never reads the alias. Every new control still passes.tests/test_cli_usage.py,scripts/prose_lint.py --diff(with and without--check sentence-length), andspec/validate.py. Pyright reports the same 69 errors as develop, message for message.🤖 Generated with Claude Code