fix(guardrails): scope block-hook-bypass echo redirect to the actual producer (#546)#568
Conversation
…producer (#546) `block-hook-bypass`'s `echo > file` heuristic matched any Bash command string containing both an `echo` (or `>`) token, conflating the `echo "content" > file` content-authoring bypass with legitimate commands that capture a subprocess's stdout to a data file while an unrelated `echo` status line appears in the same compound call. Replace the anywhere-co-occurrence with producer-scoped detection: split the literal-stripped command into simple-command segments on shell separators and flag only a segment whose command word is echo/printf AND that redirects stdout into a real file. Leading compound-command keywords (`do`/`then`/`else`/`{`) are peeled so a producer in a loop, conditional, or brace-group body is still caught. The literal-strip now carries an open quote across physical lines, so a multi-line quoted argument (a `gh issue create --body "…"` payload) stays inert instead of leaking its tokens. `printf … > file` is now caught alongside `echo`. Adds 11 regression tests (48 total pass) covering the three issue false positives and the true-positive forms including compound-command bodies. Patch-bumps guardrails 0.8.0 → 0.8.1 with a CHANGELOG entry. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 5m 5s —— View job PR Review — fix(guardrails): scope block-hook-bypass echo redirect to the actual producer
Overall AssessmentThe producer-scoped approach is the right fix for the reported false positives. The implementation is sound, the multi-line quote tracking is correctly implemented, and the 11 new test cases cover the right true-positive / true-negative split. One confirmed coverage regression (group-level redirects) is worth a call-out before merge. Finding 1 — Medium: Group-level stdout redirects no longer blocked
Trace through Segments after whitespace-strip and keyword-peel:
Neither segment satisfies both predicates → In bash, The root cause is that Test gap: Permalink to Finding 2 — Low:
|
| Severity | Finding |
|---|---|
| Medium | Group-level stdout redirects ({ echo x; } > file, ( echo x ) > file) silently unblocked — confirm whether accepted gap or fix needed |
| Low | _echo_devnull order dependency is correct but implicit — no action needed |
The fix solves the documented false positives correctly. The group-redirect regression is the one gap worth a decision before merge: if it's accepted, it should be documented as a known floor (like the command-substitution case in the script header). If not, } and ) need to be excluded from seps (or handled via a look-ahead) and tested.
The comment-hygiene check flags in-code issue-tracker references. The producer-scoped redirect section header carried an "issue #546" ref; scope the header to the behavior it exercises. The surrounding comment block already documents the producer-scope intent in full. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 53s —— View job PR Review — fix(guardrails): scope block-hook-bypass echo redirect to the actual producer
What This Push Contains
Outstanding Finding from Prior Review — Still OpenMedium: group-level stdout redirect not blocked ( This was the only medium finding in the prior review and it is not addressed in either commit on this branch. Tracing the current code:
Neither segment satisfies both predicates → not blocked. The existing test at line 96 covers redirect inside the group ( Recommendation before merge: take one of these two paths:
Path 1 unblocks merge without creating a silent regression. Path 2 improves coverage but is out of scope for what started as a false-positive fix. Everything Else Looks GoodThe core changes from commit
|
…rect floor
Address CI comment-hygiene gate (tracker-ref:repo-issue): rephrase two
test comments to describe the false-fire scenario in plain prose without
citing tracker issue numbers.
Also document, per bot review, the accepted-floor limitation surfaced on
the PR: a redirect applied to a GROUP (`{ echo x; } > file`,
`( echo x ) > file`) is not caught because the closing `}`/`)` are
segment separators, so the redirect is a separate segment from the echo
inside. Catching it needs brace/paren-depth tracking, out of scope for a
false-positive fix. Add a SCOPE note beside producer_redirect_bypass and
an accepted-floor test.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 38s —— View job PR Review — fix(guardrails): scope block-hook-bypass echo redirect to the actual producer
Overall AssessmentThe implementation is correct, the prior medium finding is now resolved, and the PR is ready to merge. The two root-cause changes ( Prior Medium Finding — Now Resolved
This follows "Path 1 — document as accepted floor" from the prior review. The gap is structurally unusual for LLM output and would require brace/paren-depth tracking to fix, which is genuinely out of scope for a false-positive fix. Status: closed. ✓ Logic Trace —
|
| segment (stripped) | _producer_head? |
_echo_file_out? |
|---|---|---|
bash fetch.sh 526 > pr526.json |
✗ bash ≠ echo/printf |
— |
echo |
✓ | ✗ no > in segment |
Neither segment satisfies both predicates → not blocked. ✓
True positive (for f in a b; do echo "$f" > out.txt; done):
After strip_literals (removes "$f"), segments include do echo > out.txt. Keyword-peel loop strips do, leaving echo > out.txt:
_producer_head:^(echo|printf)([[:space:]]|>)→ matchesecho✓_echo_file_out:(^|[^0-9&])>>?[[:space:]]*[^|&>[:space:]]→ matches> out.txt(space before>is not in[0-9&],oafter is not in[|&>[:space:]]) ✓_echo_devnull: not/dev/null→ no exemption ✓
Returns 0 → blocked. ✓
Keyword-peel regex at line 233: ^(do|then|else|\{)([[:space:]]|$). In bash ERE, \{ is literal { — correct for matching a brace-group opener. ✓
Minor Observations (No Action Required)
-
_echo_devnullordering is load-bearing but correct. Lines 237–239:_echo_file_outcan match/dev/null(the/satisfies[^|&>[:space:]]), so_echo_devnullmust come after — and it does. Noted by the prior review, no change needed. -
echo >> /dev/stderrwould be blocked._echo_file_outmatches/dev/stderr(it's not/dev/null, so_echo_devnulldoesn't exempt it). This is a pre-existing edge case unrelated to this PR's changes, and redirecting to/dev/stderrvia a file path is rare enough to be acceptable. -
Telemetry
formunchanged. Bothechoandprintflog"echo-redirect"— line 263. The PR description calls this out explicitly as intentional; the schema/example files are correctly left unchanged. ✓
Checklist
| Item | |
|---|---|
| ✓ | strip_literals multi-line quote carry is correct end-to-end |
| ✓ | producer_redirect_bypass producer-scoped segmentation is correct |
| ✓ | Keyword-peel handles do/then/else/{ bodies correctly |
| ✓ | printf added to _producer_head (was missing in old guard) |
| ✓ | No-space forms (echo>file, echo>>file) still blocked |
| ✓ | Prior medium finding (group-level redirect) resolved via SCOPE comment + accepted-floor test |
| ✓ | 48 tests pass, 11 new cases covering the documented false positives and true positives |
| ✓ | Version bump 0.8.0 → 0.8.1 — correct semver for a bug fix |
| ✓ | CHANGELOG entry matches existing format |
Approved. No blocking issues remain. The fix is a correct narrowing of the guard's match surface, every prior true positive is still caught, and the one structural gap (group-level redirect) is now a named, tested floor rather than a silent regression.
Post-green review pass (work lane)Bot review confirmed the medium finding (group-level redirect not blocked) is resolved — documented as an accepted floor via a SCOPE comment + explicit test case, per the reviewer's own lower-effort recommendation. "Status: closed." Orchestrator re-confirmed the diff since the last rule-10 read is comment/test-only, no new hook logic. Moving this out of draft. 🤖 Generated with Claude Code |
|
Claude finished @kyle-sexton's task in 1m 53s —— View job PR Review — fix(guardrails): scope block-hook-bypass echo redirect to the actual producer
Overall AssessmentReady to merge. Three prior automated reviews on this branch identified and resolved all findings before it was moved out of draft. This pass confirms nothing has regressed. The implementation is correct, the one structural gap (group-level redirects) is now a named, tested floor rather than a silent regression, and all 48 tests remain green. What Changed Since the Last Review
Logic Traces
After literal-strip (removes
Neither segment satisfies both predicates → not blocked. ✓
Group-level redirect (
Checklist
Approved. The fix is a correct narrowing of the guard's match surface: the documented false-positive forms are now allowed, every documented true-positive form is still blocked, and the one structural floor is intentional and visible in CI. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5f1597eaad
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…an (#568) The producer-scoped redirect check matched only a segment's first token, so a producer hidden behind a valid shell prefix bypassed the guard: FOO=bar echo, command echo, builtin printf, and env echo all redirected stdout into a real file yet exited 0. Peel environment assignments and the command-name modifiers command/builtin/exec/env at the segment head before the echo/printf check. Peeling is block-safe -- the producer gate still requires echo/printf, so revealing a non-producer command word never causes a block (command ls > out and FOO=bar make > log stay allowed). External command-runner utilities that carry their own options (nohup/nice/time/timeout/sudo/xargs, non-bare env) are an accepted, documented floor, mirroring the group-redirect floor. Addresses chatgpt-codex-connector P1 review finding. 57 tests pass (8 new). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 31s —— View job PR Review — fix(guardrails): scope block-hook-bypass echo redirect to the actual producer
Overall AssessmentReady to merge. The latest commit ( Codex P1 Finding — Now ResolvedThe P1 finding was that Fix: _cmd_prefix='^([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*|command|builtin|exec|env|do|then|else|\{)([[:space:]]|$)'The peel loop at lines 255–258 now uses Logic traces:
Infinite-loop safety. Each Multi-level prefix. Accepted floor documented. The SCOPE comment at lines 232–240 names the residual gap (external runners: Prior Findings — Status
Checklist
Approved. The command-prefix bypass is closed, the fix is block-safe (no new false positives), and the two accepted floors are documented and CI-visible. No blocking issues remain. |
Babysit triage — classification pass (worker lane)Re-derived open-finding state from live GitHub (all 3 comment surfaces), classified per finding, fixed the one genuinely-open branch-owned regression, and pushed
Scope note: MERGE/APPROVE are out of scope for this automated worker lane — a human merges after review. This pass fixed the open P1 regression, disclosed the residual external-command-runner floor, and resolved the codex thread. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3dd4c61190
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ucer scan (#568) Two more producer-scoped bypass forms the narrowed detector missed, both caught by the prior anywhere-in-command detector: - fd-duplication redirect before the stdout redirect: `echo x 2>&1 > file` and `echo x >&2 > file` split on the dup's `&`, orphaning the trailing `> file`. Protect the `&` in a redirect (`>&`, `<&`, `&>`) from the control-operator split so the simple command stays one segment; `&&` and background `&` still split. - compound-command headers / negation before a producer: `! echo x > file`, `if echo x > file; then ...`, and `while`/`until`/`elif` forms. Peel if/elif/while/until/! at the segment head alongside the existing do/then/else/{, completing the before-command keyword class. No new false positives (peel/protect is block-safe -- the gate still requires echo/printf). Addresses two chatgpt-codex-connector P1 findings on 3dd4c61. 66 tests pass (9 new). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 4m 1s —— View job PR Review — fix(guardrails): scope block-hook-bypass echo redirect to the actual producer
Overall AssessmentReady to merge. The latest commit ( Codex P1-1 — fd-duplication redirects: Resolved ✓Finding: Fix ( Traced logic —
Traced logic —
Codex P1-2 — Compound-command headers and
|
| Severity | Finding | Status |
|---|---|---|
| P1 (Codex) | fd-dup redirect splits producer from > file (echo x 2>&1 > file) |
Resolved ✓ |
| P1 (Codex) | Compound headers / ! before producer (if echo x > f, ! echo x > f) |
Resolved ✓ |
| P1 (Codex) | Command prefix bypass (FOO=bar echo, command echo, builtin printf) |
Resolved (prior commit) ✓ |
| Medium | Group-level redirect ({ echo x; } > file) silently allowed |
Resolved as accepted floor ✓ |
| Low | _echo_devnull ordering implicit dependency |
No action needed ✓ |
Minor Observations (No Action Required)
-
echo x &> real.txt(bash both-stdout-and-stderr redirect) is not caught.&>is protected as a sentinel before the split and restored, but_echo_file_outexcludes&before>via[^0-9&], so&>filedoesn't match. This is a pre-existing gap acknowledged in the code comment and unrelated to this PR's changes. -
echo >> /dev/stderr—_echo_file_outmatches/dev/stderrsince it's not/dev/null;_echo_devnullwouldn't exempt it. Pre-existing edge case, noted by earlier reviews, no change needed. -
CHANGELOG entries for all four fixes (producer-scope, prefix-peel, fd-dup, compound-headers) are accurate and formatted consistently with existing entries. ✓
Checklist
| Item | |
|---|---|
| ✓ | fd-dup sentinel approach (\x01) is correct — protects >&/<&/&>, leaves && and background & as separators |
| ✓ | Sentinel restored before per-segment scan — all checks operate on the actual shell form |
| ✓ | if/elif/while/until/! added to _cmd_prefix — peel loop now covers the full before-command keyword class |
| ✓ | Peel-loop termination proven safe — each iteration strictly shrinks seg, empty string fails the guard |
| ✓ | No new false positives — echo hi 2>&1 | cat, ls foo >&2, if grep -q x file; then ls; fi all correctly allowed |
| ✓ | strip_literals multi-line open_quote carry unchanged and correct |
| ✓ | Group-level redirect floor: SCOPE comment + exit 0 accepted-floor test |
| ✓ | External command-runner floor: SCOPE comment + nohup accepted-floor test |
| ✓ | 66 total tests (9 new: 4 fd-dup, 5 compound-header/negation) |
| ✓ | Version 0.8.1 consistent across plugin.json and CHANGELOG.md |
Approved. Both Codex P1 regressions introduced by the producer-scoped narrowing are closed. The fix is block-safe, every documented true-positive is still caught, and the accepted floors are named and CI-visible.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f7f28a3f7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…-scan bypasses (#546) strip_literals now drops an unquoted # comment to end-of-line without carrying quote state, so an unmatched quote in a comment can no longer leak a span onto the next line and strip a real producer there. The producer scan peels a leading redirect (bash allows redirections before the command word) and a bare coproc header before the echo/printf test, while the file-write test still runs on the un-peeled segment so the redirect stays the write signal. Adds regression tests for all three forms plus the mid-word/parameter-expansion # discriminators.
|
Claude finished @kyle-sexton's task in 6m 48s —— View job PR Review — fix(guardrails): scope block-hook-bypass echo redirect to the actual producer
Overall AssessmentReady to merge. The latest commit ( Deferred Codex P1-1 — Comment quote-state leak: Resolved ✓Finding: Fix ( Logic trace —
Word-boundary discriminators verified:
5 test cases (blocked/allowed, two comment-leak forms, mid-word, param-expansion, benign trailing comment) ✓ Deferred Codex P1-2 — Leading redirect before producer word: Resolved ✓Finding: Fix ( Logic traces:
Peel loop termination: Each iteration either peels a 7 test cases ✓ Deferred Codex P1-3 —
|
| Severity | Finding | Status |
|---|---|---|
| P1 (Codex) | Comment # leaks open_quote to next line |
Resolved ✓ |
| P1 (Codex) | Leading redirect before producer (> file echo x) |
Resolved ✓ |
| P1 (Codex) | coproc header not peeled |
Resolved ✓ |
| P1 (Codex) | fd-dup split (echo x 2>&1 > file) |
Resolved (prior commit) ✓ |
| P1 (Codex) | Compound headers / ! (if echo x > f) |
Resolved (prior commit) ✓ |
| P1 (Codex) | Command prefix bypass (FOO=bar echo, command echo) |
Resolved (prior commit) ✓ |
| Medium | Group-level redirect ({ echo x; } > file) silently allowed |
Resolved as accepted floor ✓ |
| Low | CHANGELOG missing three new fixes from 7eadee4 |
Open |
| Low | _echo_devnull ordering implicit dependency |
No action needed ✓ |
Checklist
| Item | |
|---|---|
| ✓ | Comment # case correctly drops to end-of-line without touching open_quote |
| ✓ | Word-boundary check for # correctly distinguishes comment from mid-word/param-expansion |
| ✓ | _leading_redir peels operator + target, keeping seg un-peeled for _echo_file_out |
| ✓ | Leading-redirect peel loop is termination-safe (each iteration strictly shrinks head) |
| ✓ | Leading input redirect (< input.txt echo x) correctly allowed (no > in seg) |
| ✓ | Leading /dev/null redirect correctly allowed via _echo_devnull exemption |
| ✓ | coproc bare keyword added to _cmd_prefix; named-form floor documented |
| ✓ | 17 new test cases across the three new fix areas plus discriminating false-positive checks |
| ✓ | All prior true-positive forms still blocked |
| ✓ | Group-level redirect floor: SCOPE comment + accepted-floor test |
| ✓ | External-runner floor: SCOPE comment + nohup accepted-floor test |
| ✓ | Version 0.8.1 consistent across plugin.json |
| ⚠ | CHANGELOG missing three new 7eadee4 fixes — should be added before merge |
Approved pending CHANGELOG update. The implementation is correct and closes all deferred Codex P1 findings. The one remaining action is documenting the three new fixes in CHANGELOG.md.
| Branch
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7eadee4868
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…producer scan The producer-scoped block-hook-bypass narrowing missed two true-positive forms Codex flagged at HEAD: an echo/printf hidden behind a command/exec modifier option (`command -p echo x > f`, `exec -a name echo x > f`) and a producer split from its redirect by a backslash-escaped separator (`echo x \; > f`, an escaped-newline continuation). Both wrote the file yet returned 0. Peel command/exec options (bash built-in help: `command [-pVv]`, `exec [-cl] [-a name]`, plus a `--` end-of-options marker), consuming exec's `-a name` value word. Peeling is scoped to command/exec, so env/builtin keep their bare-only floor. `command -v`/`-V` DESCRIBE their argument rather than run it, so `command -v echo > f` (which writes the word "echo", not echo's output) stays allowed — the producer-scoped contract holds. Protect backslash-escaped separators from the segment split so the simple command stays one segment. Also backfills the [0.8.1] CHANGELOG with the three fixes from 7eadee4 (comment quote-state leak, leading redirect before producer, coproc header) that landed in code but not the changelog. Adds regression tests across all new forms, including the describe-lookup false positives and the cross-segment / env-prefix / exec-name edge cases. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 5m 4s —— View job PR Review — fix(guardrails): scope block-hook-bypass echo redirect to the actual producer
Overall AssessmentReady to merge. Commit Codex P1-1 — Escaped separators: Resolved ✓Finding: Fix (lines 312–332): a pre-split loop replaces every Logic traces:
Escaped-newline: Unescaped separator control: 4 new test cases covering Codex P1-2 — Options of command/exec modifiers: Resolved ✓Finding: Fix (lines 235–255 + lines 365–376): after
Logic traces:
Option scoping to command/exec verified correct. Peel loop termination: Each iteration either peels a
6 new test cases covering blocked forms, allowed non-producer forms, describe-lookup false-positives, the Low Finding (Prior Cycle) — CHANGELOG Backfill: Resolved ✓Commit Prior Findings — All Resolved
Minor Observations (No Action Required)
Checklist
Approved. Both Codex P1 regressions are closed, all prior findings are resolved, the CHANGELOG is complete, and the fix is block-safe across the full documented true-positive surface. No blocking issues remain. |
Babysit triage — classification pass (worker lane)Re-derived open-finding state from live GitHub at HEAD
Independent security review of the fix caught a false positive I initially introduced ( Also noted for the maintainer: this pass added |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3b1841d497
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Round 5's quoted-redirect-target finding (comment 3616677601, thread on the strip_literals target-erasure regression) is tracked in #596 (updated) rather than continuing to churn — fix-round cap (rule 6b) reached without full convergence. Decide-and-defer: rounds 1-4 shipped (7eadee4, 3b1841d), round 5 deferred to #596 for a scoped follow-up (quoted-target fix or a real shell-tokenization pass). Stopping autonomous rounds on this PR. |
|
This was generated by AI (decide lane — parked-decision pointer). Parked default here has a briefed home — routes to #596, no separate briefThe round-5 comment took a default action (defer the That default's underlying fork — quoted-target/regex-peel patch vs. a full shell-tokenization pass for
No duplicate brief is warranted. #596 is the ratification carrier; the operator's ruling there discharges this parked default. This is a security-guard design fork (comment-# quote-carry / shell-tokenization), so it stays operator-owned — this lane is comment-only and is not resolving the thread or changing labels. |
The producer-scoped block-hook-bypass narrowing failed OPEN on the most common real write form: a quoted redirect target. strip_literals drops quoted spans so their tokens stay inert, but it also dropped a quoted redirect TARGET, leaving the segment as `echo x > ` with no surviving operand -- so _echo_file_out (which requires a non-space target) did not match and the write returned 0. Thus `echo x > "$out"`, `echo x > 'out.txt'`, and `printf y > "$file"` all wrote real files while bypassing the Write/Edit hooks. The pre-narrowing anywhere-in-command detector caught these. Fix at the root in strip_literals: a quoted span that belongs to a redirect operand word -- the word the quote sits in began right after a `>` -- is now kept as literal content (quote marks dropped) instead of discarded, so the write signal survives the strip. The boundary test strips the trailing operand word and its leading whitespace and checks for `>`, so partial quoting is covered too (`echo x > "$dir"/out.txt`). Keeping the literal content (not a placeholder) preserves the /dev/null exemption: `echo x > "/dev/null"` and `echo x > /dev/"null"` still resolve to /dev/null and stay allowed, so the fix adds no false positive. A quoted span anywhere else (prose, a `--body "..."` payload, a quoted echo argument) still drops, so the #546 false-positive fix is untouched. Adds 11 regression tests: quoted var/literal targets (echo and printf, with and without spaces, append form), partial quoting, the quoted and partially-quoted /dev/null discards (allowed), a non-producer stdout capture to a quoted sink (allowed), and a quoted echo argument that must not be mistaken for the target. Full suite 112/112; shellcheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHLeuVpB2ezBDGiZqDdn23
Resolves the guardrails version and CHANGELOG conflicts. main advanced the plugin to 0.9.4 on an independent track while this branch carried 0.8.1; the release that lands both is 0.9.5 (one past main's current value). The branch's producer-scoping CHANGELOG bullets are folded under a single [0.9.5] heading alongside the new quoted-redirect-operand fix, and main's [0.9.4]..[0.9.0] entries are preserved; the orphan [0.8.1] heading is removed. No code conflict — main did not touch block-hook-bypass.sh or its tests. Verified after resolution: scripts/validate-plugins.sh passes (version-bump + schema), block-hook-bypass.test.sh 112/112, shellcheck / markdownlint / typos / editorconfig clean on the changed files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHLeuVpB2ezBDGiZqDdn23
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Claude finished @kyle-sexton's task in 6m 9s —— View job PR Review — fix(guardrails): scope block-hook-bypass echo redirect to the actual producer
Overall AssessmentReady to merge. Commit Deferred Codex P1 — Quoted redirect operands: Resolved ✓Finding (prior round): Fix ( tail="${out%"${out##*[[:space:]<>|&\;()]}"}" # text up to and including the last metachar
tail="${tail%"${tail##*[![:space:]]}"}" # trim trailing whitespace
[[ "${tail: -1}" == ">" ]] && open_keep=1 || open_keep=""When Logic traces — blocked forms:
Logic traces — allowed forms (no false positives):
Backslash-in-double-quote (line 169): Merge Commit — Version/CHANGELOG Conflict Resolution: Correct ✓
Prior Findings — All Resolved
Checklist
Approved. The quoted-operand bypass is closed, the fix is correct and block-safe, the CHANGELOG is complete, and all prior findings are resolved. No blocking issues remain. |
|
Claude finished @kyle-sexton's task in 10m 54s —— View job PR Security Review — fix(guardrails): scope block-hook-bypass echo redirect to the actual producer
Two confirmed security findings follow. All other paths I traced — Finding 1 — IMPORTANT (high confidence):
|
| pos | char | action |
|---|---|---|
| 7 | \ |
$'\\') case: out += "\\", i += 2. i is now 9. |
| 9 | # |
'#' case. i != 0. prev = line[8] = '\'. |
| — | — | '\' == [\;|\&\(\)\<\>] → TRUE (backslash is in the class). break. |
#> file is dropped as a "comment". out = "echo content \\". No > present. _echo_file_out does not match. Hook returns 0.
In bash, echo content \\#> file tokenizes as: echo content \# (args content and \#) with stdout redirected to file via > (which terminates the \# word). The file is written. The bypass is real.
The comment at line 206 says "A backslash-escaped # never lands here — the \ case above consumes it." That claim is correct for \# (single backslash before #). But for \\# the $'\\') case consumes only the \\ pair and advances by 2, landing i at # — which then sees prev = '\' (the second char of the \\ pair, not a separator in any shell sense) and incorrectly fires the comment break.
Fix: Replace [\;\|\&\(\)\<\>] with [;|&()<>] (without backslash). In bash bracket expressions, these characters are all literal and don't need escaping; removing the backslashes eliminates the inadvertent \ entry. The resulting class is identical in intent and correctly excludes \ from the comment-trigger set.
# Before (buggy):
[[ "$prev" == [[:space:]] || "$prev" == [\;\|\&\(\)\<\>] ]]
# After (correct):
[[ "$prev" == [[:space:]] || "$prev" == [;|&()<>] ]]Test gap: No test for echo x \\#> file or echo content \\#> file.
Permalink:
No Issues Found In
plugin.json— version bump only (0.8.0 → 0.8.1), correct semver.CHANGELOG.md— documents all behavioral changes; no security-relevant content.block-hook-bypass.test.sh— test harness usesenv … bash "$HOOK" <<<"$(command_json …)"safely; no injection surface;command_jsonpasses throughjqwith proper quoting.strip_literalsopen_keepredirect-operand logic — correctly identifies quoted redirect targets. Traced throughecho x > "$out",echo x > /dev/"null"(exempted),echo "data" > "log.txt"(blocked), andbash fetch.sh > "pr526.json"(non-producer, allowed).strip_literalsmulti-lineopen_quotecarry — correct. Heredoc-opener gate[[ -z "$open_quote" ]]prevents<<EOFinside a carried quote from stranding the stripper.strip_literals#comment logic — correct for the intended case (true # ",echo a#b). The\inclusion bug (Finding 2) is the only flaw.- fd-dup sentinel (
\x01) approach — correct;&&and background&still split; sentinel restored before per-segment scan. - Escaped-separator sentinel (
\x02) — correct;\;,\|,\&,\<newline>all protected.
| Branch
Merge-drive lane — recorded operator basis + independent verification (rule 8)This Independent verification (producer ≠ critic — the merger read the actual diff, not the subagent's self-report):
Closes #546. Squash-merging on head |
|
Operator ack (ratification interview, 2026-07-22 ~03:05Z): RATIFIED post-hoc. The operator-delegated security merge of this producer-scoping fix (block-hook-bypass, #546 false-positive) is confirmed accepted. Residual review findings remain tracked in #596 (separately accepted and closed this interview). |
Summary
block-hook-bypass.sh(PreToolUse:Bash) exists to block Bash file-write workarounds that circumvent the Write/Edit hook gates. Itsecho > filedetector matched anecho/>token co-occurrence anywhere in the command string and conflated it with theecho "content" > filecontent-authoring bypass it is meant to catch. That over-broad heuristic blocked legitimate commands that redirect a subprocess's stdout to a scratchpad data file while an unrelatedechostatus line lives elsewhere in the same compound call — and any command whose quoted string arguments merely mention the tokens (e.g. agh issue create --body "…"whose text describes the bug). Observed while PR-babysitting #526.Fix
Two root-cause changes to
plugins/guardrails/hooks/block-hook-bypass.sh:Producer-scoped redirect detection (
producer_redirect_bypass). The anywhere-co-occurrence (_echo_redirpresent AND_echo_file_outpresent) is replaced by segment analysis: the literal-stripped command is split into simple-command segments on shell separators (; | & ( )and newlines), and a segment is flagged only when its command word isecho/printfAND that same segment redirects stdout into a real file. The redirect's producer must therefore be the echo/printf — not a co-located but unrelated one. Leading compound-command keywords (do/then/else/{) are peeled first, so a producer inside a loop, conditional, or brace-group body is still caught (for …; do echo x > f; done). The existing fd/stderr (2>…,2>&1,&>) and/dev/nullexemptions are preserved unchanged.Multi-line quote-aware literal strip (
strip_literals). The per-linesedquote strip is replaced by a char-by-char scan that carries an open quote across physical lines, honoring bash quoting (single quotes take no escapes; inside double quotes a backslash escapes the next char). A--body "…"argument spanning newlines is now inert end-to-end instead of leaking itsecho/>tokens from the second line on — the exact form that forced the reporter to fall back to--body-file. Heredoc-body handling and all its regression cases are untouched (heredoc opener detection is now gated to run only outside a quoted span).printf … > filecontent-authoring is now caught alongsideecho … > file(both are the producer set named in the issue's suggested fix direction). The telemetryformstaysecho-redirect, so the logged payload shape is unchanged — the schema/example files are left alone.This is a narrowing of the guard's match, not a disabling of it: every true-positive content-authoring form still blocks (see Verification).
Verification
Both actually run in this worktree.
Test suite —
bash plugins/guardrails/hooks/block-hook-bypass.test.sh→PASS=48 FAIL=0(37 pre-existing + 11 new). New cases:Allowed (were false-positives):
script stdout capture + echo status (allowed)—bash fetch-all-pr-comments.sh 526 > pr526.json && echo "EXIT: $?"script stdout capture; echo status semicolon (allowed)poll-loop redirect + echo summary (allowed)— redirect + statusechoin a loop bodygh issue create --body mentioning echo > file, single line (allowed)gh issue create --body mentioning echo > file, multi-line (allowed)Still blocked (true positives, unchanged):
echo content > file still blocked,printf content > file (blocked)echo > file after an unrelated command (blocked)echo > file in for-loop body (blocked),… in if-then body (blocked),… in brace group (blocked)All prior heredoc / here-string / stderr-redirect / kill-switch / telemetry regression cases remain green.
Linters (repo-pinned, no suppressions added beyond the existing
SC2016command-under-test convention):shellcheck --rcfile .shellcheckrconblock-hook-bypass.shandblock-hook-bypass.test.sh→ clean (exit 0).markdownlint-cli2onCHANGELOG.md→Summary: 0 error(s).typoson the changed shell + markdown → exit 0.editorconfig-checkeron all four changed files → exit 0.scripts/validate-plugins.sh(schema + version-bump validation) →All plugin manifests and the catalog validated.Closes #546
Related
git commit -F -with inline heredoc Co-Authored-By trailer (requires BOTH markers, contradicting documented "either marker -> stay quiet" intent) #545 — the siblingflag-commit-pr-skill-bypassadvisory false-fire of the same class. Not fixed here (out of scope; it has its own separate detection logic and does not sharestrip_literals).🤖 Generated with Claude Code
Co-authored-by: Claude Opus 4.8 (1M context) noreply@anthropic.com