Skip to content

Read Config and Alias Rewrites the Write Guard Missed - #3306

Merged
ptr727 merged 6 commits into
developfrom
feature/guard-config-alias-reads
Oct 21, 2026
Merged

ptr727 merged 6 commits into
developfrom
feature/guard-config-alias-reads

Conversation

@ptr727

@ptr727 ptr727 commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

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.

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_DIR forms than a wrapped one), and #3305 (an alias carrying --no-verify). The Copilot round's over-denial from applying one wrapper's GIT_DIR to 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 introduced and now has an outcome.

Listed at the first push (pass 3 on 69e26fe):

  1. Backticks or $( inside single quotes read as a live substitution. Fixed in 45f1d00.
  2. A one-argument git config <key> counted as a write. Fixed in 45f1d00.
  3. bash -c "$CMD" before a push was not counted. Fixed in 45f1d00 (and in bodies, 03f0e87).
  4. The bodiless-shell check keyed on the shell word. Fixed in 45f1d00 and d7bae25. A script file or make run directly is the documented "a write inside a script file is unseen" limit.
  5. The command-position check covered gh but not git. Fixed in d7bae25.
  6. A README So is bound to the wrong clause. Fixed in 45f1d00.
  7. The spec READMEs lacked the new shell and substitution forms. Fixed in 45f1d00 and 03f0e87.

Raised by the passes on 45f1d00 and d7bae25, each fixed in d7bae25 or 03f0e87:

  • A shell given only options still reads its input.
  • Launchers such as flock or watch run their git word.
  • gh groups are now read past a repository flag with _gh_path.
  • Config write actions are read cut short.
  • An argument holding a run-time word, such as mkdir -p "$D", runs nothing.
  • A substitution after a backgrounded, piped, or looped push is read.
  • A gh read such as gh pr view or gh repo view no longer counts.

Raised by the pass on 03f0e87, and the design points from every pass:

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

  • Differential, develop versus branch. Both guards ran over 4,681 commands. Those are every selftest table's commands plus constructed cases. Each ran in ten modes: origin, open rules, foreign origin, no origin, persisted aliases, primary checkout with aliases, alias reads timing out (plain and primary), and a real scratch repository with code-style and open rules. That is 46,810 verdicts at 03f0e87. Deny to allow: 0. Allow to deny: 14,950. Errors: 0. Every allow-to-deny flip falls into a class one of the seven fixes names. They are HOME/XDG, hooksPath, timed-out alias reads, per-command env pairs, alias expansions past options, rule-6 alias directories, and pushes after a config rewrite.
  • Revert proof. With the code hunks reverted and the new tests kept, all 56 new deny rows and the 3 timed-out-read checks fail, and no allow row does. Three of the four rows feeding the _UNRESOLVED sentinel through the config_lookup seam raise AttributeError on develop's walk, which takes only a string. The fourth is a control that never reads the alias. Every new control still passes.
  • Gates. The selftest passes. So do ruff check, ruff format, mypy, the agent-safety pytest suite, tests/test_cli_usage.py, scripts/prose_lint.py --diff (with and without --check sentence-length), and spec/validate.py. Pyright reports the same 69 errors as develop, message for message.

🤖 Generated with Claude Code

ptr727 and others added 3 commits October 21, 2026 07:12
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>
Copilot AI lite review requested due to automatic review settings October 21, 2026 15:01
@ptr727

ptr727 commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

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

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

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 65.49%. Comparing base (848d629) to head (03f0e87).

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           
Flag Coverage Δ
python-3.13 65.49% <ø> (ø)

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.

@coderabbitai

coderabbitai Bot commented Oct 21, 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: 1f8105a7-cae0-4b2a-97c1-239a27186071
  • 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 and others added 2 commits October 21, 2026 08:17
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>

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

Unresolved guard-analysis issues can cause incorrect denials or missed safety checks.

8 open issues
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

Comment thread host-setup/agent-safety/claude/gh-write-guard.py Outdated
Comment thread host-setup/agent-safety/claude/gh-write-guard.py
Comment thread host-setup/agent-safety/claude/gh-write-guard.py Outdated
Comment thread host-setup/agent-safety/claude/gh-write-guard.py Outdated
Comment thread host-setup/agent-safety/claude/gh-write-guard.py Outdated
Comment thread host-setup/agent-safety/README.md Outdated
Comment thread host-setup/agent-safety/claude/README.md Outdated
Comment thread host-setup/agent-safety/claude/gh-write-guard.py Outdated
@ptr727

ptr727 commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

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

@ptr727

ptr727 commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

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.

  1. gh-write-guard.py _config_may_change: backticks or $( inside single quotes read as a live substitution. Fixed in 45f1d00.
  2. _git_word_changes_config: a one-argument git config <key> counted as a write. Fixed in 45f1d00.
  3. _config_may_change: bash -c "$CMD" before a push was not counted. Fixed in 45f1d00 and 03f0e87.
  4. _config_may_change: the bodiless-shell check keyed on the shell word. Fixed in 45f1d00 and d7bae25. A script file or make run directly is the documented script-file limit.
  5. _config_changing_word: command position covered gh but not git. Fixed in d7bae25.
  6. README.md alias-options sentence: So is bound to the wrong clause. Fixed in 45f1d00.
  7. Both READMEs lacked the new shell and substitution forms. Fixed in 45f1d00 and 03f0e87.

The later passes' findings are each Fixed in d7bae25 or 03f0e87, except two groups, as the body lists. The run-time-under-launcher, eval-in-body, xargs ... bash -c, and deny-message forms are Deferred to #3307. The shared-walker and duplicate-helper design points are Deferred to #3308.

@ptr727
ptr727 merged commit d2ad5cf into develop Oct 21, 2026
13 checks passed
@ptr727
ptr727 deleted the feature/guard-config-alias-reads branch October 21, 2026 16:57
ptr727 added a commit that referenced this pull request Oct 21, 2026
… 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)
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