Skip to content

Read Every Launcher and Exported Git Directory in the Write Guard - #3342

Merged
ptr727 merged 7 commits into
developfrom
feature/guard-directory-tokenizer-launchers
Oct 22, 2026
Merged

ptr727 merged 7 commits into
developfrom
feature/guard-directory-tokenizer-launchers

Conversation

@ptr727

@ptr727 ptr727 commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

Closes the write guard's launcher and exported-directory allow gaps. Every new reading only adds denials, and none narrows an existing deny. This is the first of two sequential lane PRs. The second takes the tokenizer and directory-value issues (#2991, #2995, #3090, #3074, #3075) once this one merges.

Issue Status

Fixes #3050
Fixes #3083
Fixes #3085
Fixes #3304
Refs #3307
Refs #2943
Part of #3324

#3307 stays open for two items. One is a run-time command word behind a launcher the guard does not know, as in uv run "$CMD". It cannot be told apart from an argument such as mkdir -p "$D", which the issue requires to stay allowed. The other is the config-change deny reason, a message rather than an allow gap. #2943 stays open for the items other lanes own.

Verification

  • Differential. Develop's guard and this branch's guard each classified every command the branch's self-test classifies, with the same seams and pinned globals (the MSYS flag and the swapped _git_stdout, _live_branch_rules, _origin_owner_repo, and _current_push_branch included). That was 1654 classify calls over 1540 distinct commands:
    • deny to allow: 0
    • allow to deny: 43, each one a new or changed self-test deny case, one per fix above. One changed case is the old "only a leading export is read" accepted gap, which Read a Direct Git Command's GIT_DIR as a Wrapper's Is Read in the Write Guard #3304 closes.
    • unchanged: 493 allow on both, 1118 deny on both
    • The local review passes also probed constructed shapes outside that corpus against both guards. They found three deny-to-allow flips, each fixed with a regression case: watch and coproc in front of a push, and an assignment-shaped find argument replacing a gh write's GH_REPO. One more flip is correct and kept: flock <file> env GH_REPO=<repo> gh ... now reads the GH_REPO that env really passes, where develop read the shell's.
  • Revert proof. The 43 cases above are the new deny tests, and each gives allow on develop's guard, so each fails with the fix reverted. Four more new deny cases are regression guards develop already denies: watch -n 60 git push origin develop, the subshell-export case, and the two find ... -name GH_REPO=... cases. The 14 new allow controls allow on both guards.
  • Self-test. gh-write-guard.py --selftest passes.
  • Install tests. test_install.py passes, 163 tests run, 1 skipped.
  • Gates. ruff format, ruff check, mypy, prose_lint.py, repo_gate.py --check eol, and docker_lint.py pass.
  • Local review. Seven frontier-tier passes, each recorded with local_review.py. The last found no deny-to-allow flip. Every finding got an outcome below.

Remaining Local Review Findings

🤖 Generated with Claude Code

ptr727 and others added 3 commits October 21, 2026 20:38
Read a git command, a foreign shell, and a run-time command word behind a
launcher the guard did not walk past, such as flock with its lock file,
watch, winpty, coproc, while, or until. A launcher the guard does not know
is read the safe way round, with an env after it included.

Read each launcher in front of a push, so one that keeps its directory no
longer hides one that moves it. Read a direct git command under an export
anywhere before it and under its whole prefix, beside the reading without
them. Read eval, source, and a bodiless -c in the config-change scan.

Fail closed where rule 6 or a gh write sits in a wrapper body past the
nesting cap, and clear GIT_INDEX_FILE in the linked-worktree install test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the first-launcher reading of a push beside the launcher walk, so a
push that watch hands to a shell stays undetermined. Read each export state
before a git command as a reading of its own, a bare-name export and
declare -x included. Read a -c shell whose string xargs or a later word may
supply, and builtin source, in the config-change scan.

Read a gh write past the nesting cap at the argv level too, and name the
rule 6 subcommand set once for the verdict and the nesting-cap check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ite Guard

Re-read a git command from the values its own options and prefix set, so
a leading export no longer pins the reading against a later one. Read the
exports of a command of plain assignments through the existing export and
assignment readers, an export a builtin runs, and the outer command's
exports for a git command in a command string.

Read a watch anywhere in front of a push as handing it to a shell. Read a
-c shell's string as supplied by xargs only under -I, -i, or --replace.
Read a shell running a script file and a run-time word opening a command
in the config-change scan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings October 22, 2026 04:27
@ptr727

ptr727 commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

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

@coderabbitai

coderabbitai Bot commented Oct 22, 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: 35144028-0e7b-4290-acc0-886932e78418
  • 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.

@codecov

codecov Bot commented Oct 22, 2026

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff             @@
##             develop    #3342   +/-   ##
==========================================
  Coverage           ?   65.53%           
==========================================
  Files              ?       18           
  Lines              ?    10036           
  Branches           ?        0           
==========================================
  Hits               ?     6577           
  Misses             ?     3459           
  Partials           ?        0           
Flag Coverage Δ
python-3.13 65.53% <ø> (?)

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

Unresolved launcher parsing and export-state issues can allow unsafe commands or produce incorrect denials.

2 open issues
What changed in this PR

This pull request hardens the write guard by inspecting more launcher chains, foreign shells, exported Git directories, and deeply nested commands before allowing Git or GitHub writes.

Changes:

  • Extends launcher, shell, environment, export-state, and nested-command analysis.
  • Adds fail-closed handling and regression coverage for additional Git and GitHub write paths.
  • Updates safety documentation and clears inherited GIT_INDEX_FILE state in linked-worktree tests.
File Summary
host-setup/​agent-safety/​README.md Documents expanded launcher, export, wrapper, and nesting behavior.
host-setup/​agent-safety/​claude/​test_install.py Clears GIT_INDEX_FILE during linked-worktree test setup.
host-setup/​agent-safety/​claude/​gh-write-guard.py Implements expanded command parsing, launcher traversal, export propagation, and nesting checks.

Review effort: Lite

Comment thread host-setup/agent-safety/README.md Resolved
ptr727 and others added 2 commits October 21, 2026 21:35
A leading export is read as the git command's own value, so it gives no
reading without it. Say so in requirement 6, and drop the accepted-gap
wording from the leading-export reader now that a later export is read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cher

Behind a launcher the guard does not know, read an assignment as git's or
gh's environment only after an env among the launcher's words. Any other
assignment there is the launcher's own argument, so it no longer replaces
the GH_REPO a gh write runs under. Read a foreign shell there only where it
is given its command string option, so a shell name another command takes
as an argument, as in go build ./cmd, is no shell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 and others added 2 commits October 21, 2026 21:57
…ite Guard

Read GH_REPO behind an unknown launcher as before, from the assignments in
front of it only, so an env-named path among the launcher's words cannot
replace the shell's GH_REPO. Read a run-time word as a command after `{`
rather than after any command-position word, so `docker exec "$C" ls` runs
nothing. Read a -c string an xargs -I runs as replaced only where it holds
the replace string. Share one helper for a shell run with no command string,
and say in the docs that an export in a subshell is still read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uard

A bare --replace replaces the default {}, so read it that way. Drop the
reading of a run-time word after `{` as a command: every real brace group
is already read through the command prefix, and an argument `{` is not one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ptr727

ptr727 commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

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

@ptr727
ptr727 merged commit 7bdc2f5 into develop Oct 22, 2026
13 checks passed
@ptr727
ptr727 deleted the feature/guard-directory-tokenizer-launchers branch October 22, 2026 05:40
ptr727 added a commit that referenced this pull request Oct 22, 2026
…e Guard (#3345)

## Summary

Closes the write guard's tokenizer, directory-value, launched-string,
and expanded-body allow gaps. Every new reading only adds denials, and
none narrows an existing deny. This is the second of two sequential lane
PRs, after #3342.

- **#2991, a quote pair spanning two comments.** shlex ignores comments,
so two comments each holding an apostrophe pair up as one quoted span
and hide the lines between them. Where a `#` opening a word has a quote
after it on its line, `_shell_tokens` adds `_context_lex`'s
comment-aware tokens wherever shlex's do not hold every one of them.
- **#2995, directory values and the tokenizer.**
- A command naming git or gh that holds a carriage return fails closed,
since bash reads one as part of a word.
- A word a backslash-newline splits fails closed on any git or gh
command, not only a push. So does an escaped backslash before a line
break, which bash reads differently from the guard's fold. git or gh is
named through quoting too, as `_names_push` reads a push.
- The text an `eval` runs and the string `watch` hands a shell are read,
so `eval git -C "$W" status` is denied.
- An unquoted pathname pattern in a `-C`, `--git-dir`, or `--work-tree`
value is denied, literal or after a run-time word.
- **#3090, `env -S`.** `_launched_command_string` reads the string an
`env -S`, `-S<string>`, clustered `-iS`, or `--split-string` splits into
a command, and the string a `watch` or a `flock -c` hands a shell. An
option taking a value before it is read as one.
- **#3074, words xargs appends.** A push, a commit, or a `gh pr merge`
that `xargs` runs or may run fails closed, since `xargs` appends words
the guard does not read. The merge is read by its command path, so
`xargs gh pr comment 5 --body merge` runs no merge.
- **#3075, a double-quoted wrapper body.** A body the outer shell has
already expanded is an `_ExpandedText`. That covers a double-quoted body
with an unescaped `$` or backtick, a `watch` command, and an `eval`
payload written outside single quotes. `_quoting` then reads every `$`
word in it as one that word splitting may split, a literal quote left in
a word included. One change covers the merge and commit sites.

## Issue Status

Fixes #2991
Fixes #2995
Fixes #3090
Fixes #3074
Fixes #3075

## Verification

- **Differential.** Develop's guard and this branch's guard each
classified every command the branch's self-test classifies, with the
same seams and pinned globals, as #3342 did. That was 1701 classify
calls over 1587 distinct commands:
  - **deny to allow:** 0
- **allow to deny:** 31, each one a new self-test deny case, one per fix
above
  - **unchanged:** 508 allow on both, 1162 deny on both
- The local review passes also ran transform differentials outside that
corpus, such as wrapping every develop case in comment pairs, `eval`,
`watch`, `flock -c`, `env -S`, and a double-quoted `sh -c`. They found
three deny-to-allow flips, each fixed with a regression case: a
double-quoted body's escaped backslash before a line break, a quoted git
word hiding a split word, and a dashed git binary such as `git-push`
beside a split word.
- **Revert proof.** The 31 cases above are the new deny tests, and each
gives allow on develop's guard, so each fails with the fix reverted. The
new controls allow on both guards.
- **Self-test.** `gh-write-guard.py --selftest` passes.
- **Install tests.** `test_install.py` passes, 163 tests run.
- **Gates.** ruff format, ruff check, mypy, `prose_lint.py` (with and
without `--check sentence-length`), `repo_gate.py --check eol`, and
`docker_lint.py` pass.
- **Local review.** Six frontier-tier passes, each recorded with
`local_review.py`. The last found no deny-to-allow flip. Every finding
got an outcome below.

## Remaining Local Review Findings

- **Filed as #3344 (introduced).** The backslash-newline and
escaped-line-break denies read no quoting or heredoc data, so a literal
one inside single quotes or a quoted heredoc is denied. Two comments
with apostrophes leave the union's quoting unknown, so a quoted `"$X"`
beside them is denied. An operator glued to a line continuation, as in
`git add x &&\` before a `git commit` line, reads as a split word. A
`watch` reads its own option values and `-x` as expanding its command.
An eval payload is marked expanded per text rather than per eval. Near
variants still allowed, each shared with develop: `xargs git` and `xargs
gh pr` with the subcommand on input, GNU env long-option prefixes such
as `--split=`, `\$` inside a double-quoted body, and GNU `parallel`.
- **Filed on #3344 as well.** The README's wording of the `xargs` merge
deny, which should say "runs or may run". `xargs -I{} sh -c 'gh pr merge
{}'`, where the inner shell splits the replaced line, and macOS
`gxargs`.
- **Declined on this pull request with evidence.** flock's attached
`-c<string>` and `--command=<string>`, which util-linux flock runs as a
program name, and git's attached `-C<dir>`, which git rejects as an
unknown option.
- **Declined, design.** Folding `_GIT_OR_GH_WORD` into `_NAMES_GH`,
whose `$` branch only gates the nesting-cap walk. `_outer_expands` and
`_unquoted_pattern` sit beside `_expands_unquoted` because each asks a
different question, an expansion inside double quotes and a pattern
character. The env cluster walk sits beside `_option_takes_next`, which
says only whether an option takes the next word, not where an `S` ends
the cluster.
- **Filed, pre-existing, from #3342.** #3336, #3337, #3339, #3340,
#3341, #3343.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Oct 22, 2026
… Narrowing to Main (#3346)

## Summary

Promotes these pull requests from develop.

- [#3329](#3329): restores
the prerelease filter on the GitHub Pre-Release shield and has the audit
require it.
- [#3333](#3333): narrows
the prose gate's comment-added promotion stand-down by workflow model.
- [#3335](#3335): shares
one git location variable list across the self-tests.
- [#3342](#3342): the
write guard reads every launcher and exported git directory.
- [#3345](#3345): the
write guard reads tokenizer, directory-value, and launched-string gaps.

Refs #3307
Refs #2943

Closes #3320
Closes #1663
Closes #3324
Closes #3050
Closes #3083
Closes #3085
Closes #3304
Closes #2991
Closes #2995
Closes #3090
Closes #3074
Closes #3075

🤖 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