Repository navigation
Read Every Launcher and Exported Git Directory in the Write Guard - #3342
Conversation
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>
|
A recorded local strict-review pass covers head |
|
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 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #3342 +/- ##
==========================================
Coverage ? 65.53%
==========================================
Files ? 18
Lines ? 10036
Branches ? 0
==========================================
Hits ? 6577
Misses ? 3459
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 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_FILEstate 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
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>
…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>
|
A recorded local strict-review pass covers head |
…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>
… 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)


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.
_repo_may_movewalks every launcher in front of git with_prefix_walk, not only the first. Sotimeout 60 uv run --directory <dir> git pushandsudo uv run ...are judged as moved. The first-launcher reading is kept beside the walk. Awatchanywhere in front of git hands it to a shell, so it moves the push too._foreign_shell_atreadspwsh,fish,cmd, and the rest behind a launcher it does not know, the safe way round, as_launcher_prefixreads one, where the shell is given its command string option. Sowinpty pwsh -Command '...'anduv run fish --command='...'bodies are read, whilego build ./cmdreads no shell._prefix_walkpassesflock <file>,watch,winpty,coproc,while, anduntilwith their options and positional words._git_word_assignmentsfalls back to_launcher_prefixbehind a launcher it does not know, and that reads the assignments after anenvamong the launcher's words. Any other assignment there is the launcher's own argument. The GH_REPO readers keep develop's launcher reading, since they take a prefix in place of the shell's state.GIT_DIR. A git command is read again under each export state before it, not only a leadingexport ... &&. That includes a bare-nameexport NAMEof an earlier assignment, adeclare -x, and an export abuiltinruns. It is also read under its whole prefix past a precommand or launcher, and a command-string body under the outer command's exports. Each is a reading beside the one without it.flock,watch,coproc,while, oruntil. It reads anevalorsourceinside a wrapper body or a substitution, and a shell there running a script file. It reads a-cshell with no string, one whose next word is an option, and one whose string holds the string anxargs -I,-i, or--replacereplaces.gh apicall there, read by regex and at the argv level through_names_gh_write.TestAliasReadThroughLinkedWorktreeGitfilealso dropsGIT_INDEX_FILE.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 asmkdir -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
_git_stdout,_live_branch_rules,_origin_owner_repo, and_current_push_branchincluded). That was 1654 classify calls over 1540 distinct commands:watchandcoprocin front of a push, and an assignment-shapedfindargument 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 thatenvreally passes, where develop read the shell's.watch -n 60 git push origin develop, the subshell-export case, and the twofind ... -name GH_REPO=...cases. The 14 new allow controls allow on both guards.gh-write-guard.py --selftestpasses.test_install.pypasses, 163 tests run, 1 skipped.prose_lint.py,repo_gate.py --check eol, anddocker_lint.pypass.local_review.py. The last found no deny-to-allow flip. Every finding got an outcome below.Remaining Local Review Findings
set -aandlocal -x, an export after a git word a function or loop runs first, a clusteredxargs -0I{}, a wrapper body's config scan skipping_string_read_elsewhere, an assignmentsudopasses behind an unknown launcher,uv run env GH_REPO=... gh ..., and a run-time word in a named coproc's group.command -vlooks up reads as a shell run with no string inside a substitution or body. Awatch-named lock file in front of a push reads aswatch. Its first item, every assignment after an unknown launcher read as git's environment, is fixed here._leading_export_dirsno longer calls a later export an accepted gap.go build ./cmd,docker exec "$C" ls, and anxargs -I{} sh -c '... "$1"' _ {}are no longer over-denied._exported_statesinto one export-state primitive for every reader is Read Every Export Form for Git Config and Wrapper Environment in the Write Guard #3339's direction rather than this change's. Folding_LAUNCHER_POSITIONALSinto_EVAL_RUNNERSor_COND_PRECEDERSwould change rule 7's and the eval reader's readings. Dropping the_STAYING_LAUNCHERStail of_repo_may_moveflipstimeout.exe,Sudo, andcoprocpushes from deny to allow against develop._xargs_runswalks back itself because_run_startreturns None after a redirection or a).watch's-qtakes a required value, perwatch --help.GIT_DIRvalue read literally), twoxargsconfig-change shapes noted on Read the Config-Change Scan's Remaining Run-Time, Eval, and Xargs Forms in the Write Guard #3307, Read Words xargs Appends to a gh Write in the Write Guard #3336 (wordsxargsappends to a gh write), Read Commands an Outer Variable Adds to a Double-Quoted Wrapper Body in the Write Guard #3337 (an outer variable adding commands to a double-quoted wrapper body, with a decision), Read Every Export Form for Git Config and Wrapper Environment in the Write Guard #3339 (the other export readers read onlyexport NAME=value).🤖 Generated with Claude Code