Repository navigation
End the Guard's Stdin Redirect Scan at a Trailing Comment - #2227
Conversation
The wait-loop rule credited a redirect written inside a trailing comment as the loop's input bound, so `yes | while read l; do sleep 30; done # < f` was allowed although bash reads nothing after the `#`. The scan now ends at a word opening an unquoted comment, and at a redirect target that opens one, using the quoted mask so a quoted `#` still names a file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ending the scan at a `#` it could not be sure was a comment skipped the
later binding that applies, as in `< ${g:- #x} < /dev/zero` or a quoted
`'#x'` whose quoting the mask could not read, both allowed although
develop denied them. A comment now keeps an earlier binding only where the
quoting is known and no expansion precedes it, and a redirect whose target
opens a comment bounds nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lex splits words at a carriage return and bash does not, so in `log<CR>#x` the `#` sits inside a word yet arrived as a token of its own, ending the scan before a later `< /dev/zero`. The caller now passes no mask for such a command, so no comment keeps an earlier binding. 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 #2227 +/- ##
==========================================
Coverage ? 56.61%
==========================================
Files ? 16
Lines ? 7482
Branches ? 0
==========================================
Hits ? 4236
Misses ? 3246
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.
Copilot review overview
🔵 Needs a closer look
It modifies a security-critical agent-safety guard whose correctness depends on subtle shell-tokenization and quoting-mask behavior, warranting final human verification despite no defects being found.
Review effort: Balanced
Findings: None
What changed in this PR
This PR fixes a false-negative in the hub's Claude write guard (host-setup/agent-safety/claude/gh-write-guard.py). The _redirects_stdin scan, which decides whether a while read ... done wait loop is bounded by a finite input, previously credited a redirect written inside a trailing shell comment (e.g. ... done # < f or ... done #< f) as the loop's input bound. Since bash reads nothing after #, such a loop actually drains its upstream pipe forever and should be denied. The guard is part of the fleet's agent-safety layer that blocks unbounded poll loops, so this closes a real bypass.
The fix ends the scan at an unquoted-# word the same way it already ends at a separator or reserved word, using the quoting mask the caller already computes to distinguish a real comment from a quoted #f filename, and it is deliberately deny-safe where quoting is unknown or an expansion that can hold a space precedes the #.
Changes:
- Add a
quotedmask parameter to_redirects_stdin/_reads_its_inputand thread the existing per-token mask from_unbounded_wait_loop(passingNonewhen the command contains a carriage return, which the lexer splits on but bash does not). - End the redirect scan at an unquoted
#word, treat a redirect whose target opens a comment as unbound, and only carry an earlier binding past a#when the comment is certain. - Add nine
--selftestcases covering trailing/glued comments, comment-as-target, payload comments, quoted#,#inside an expansion, unknown quoting, and the carriage-return shape.
| File | Description |
|---|---|
| host-setup/agent-safety/claude/gh-write-guard.py | Ends the stdin-redirect scan at an unquoted trailing comment and threads a quoting mask so a quoted # still names a file, plus selftest coverage for each shape. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
## Summary Promotes develop to main, carrying the pull requests below, each already reviewed and merged into develop. - [#2203](#2203): Strip every heredoc body a line opens in the wait-loop rule. - [#2216](#2216): Point the charset-unknown message at where a tier is classified. - [#2218](#2218): Name the leftover old tree when the Bash loader cannot remove it. - [#2227](#2227): End the guard's stdin redirect scan at a trailing comment. - [#2230](#2230): Tell apart the causes a labels payload is refused for. - [#2232](#2232): Correct the task names and pointers in the Python tasks snippet header. - [#2234](#2234): Drop the stale private-repository note from PhotoCleaner's registry entry. - [#2237](#2237): State the bootstrap's archive live channel in two skills. - [#2239](#2239): Stop the promotion count on a failed fetch in backlog-burndown. - [#2241](#2241): Render the include walk once per `build_dist.py --check` run. - [#2243](#2243): Have the tree check call `escapes_repo_root` and refuse a symlinked component. - [#2247](#2247): Refuse a Windows drive component anywhere in a tree path. - [#2222](#2222): Tick the adopted repos in the merge-bot and gate rollout stages. - [#2121](#2121): Refuse a dangling remote-tracking target in `local_review`. - [#2249](#2249): Ignore a quoted separator when the guard finds a loop's `done`. Closes #2112 Closes #2171 Closes #1790 Closes #2152 Closes #1372 Closes #2149 Closes #1082 Closes #1772 Closes #1310 Closes #1379 Closes #1452 Closes #2244 Closes #2168 Closes #2106 Closes #2207 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Summary
The wait-loop rule's
_redirects_stdinscan credited a redirect written inside a trailing comment as the loop's input bound, soyes | while read l; do sleep 30; done # < f(and the#< fspelling) was allowed although bash reads nothing after the#.#words are quoted, sodone < '#f'still names a file.< ${g:- #x} < /dev/zero.log<CR>#xis one word rather than a comment.--selftestcases cover each shape. Reverting the fix with the cases kept fails the new deny cases, and passing no mask fails the quoted-#allow case.A local strict review pass also found a pre-existing sibling, a separator or parenthesis inside a target expansion ending the scan early, filed as #2225.
Closes on promotion: #2152
🤖 Generated with Claude Code