Summary
strip_literals in plugins/guardrails/hooks/block-hook-bypass.sh drops a quoted span's content
without leaving any mark when the span is not a redirect operand. Where that span sits inside a
command word, the two sides of the word are spliced together and can form a word bash never forms —
including the producer names _producer_head matches, so a command bash would run as ecxyho
blocks as an echo … > file write.
This is a pre-existing defect, not a regression: measured below, the single-line spelling has
been rc=2 since well before the change that surfaced it.
Evidence
Hook invoked as a decision function on PreToolUse Bash payloads (rc=2 blocked, rc=0 allowed).
base is 4c90b454 (guardrails 0.27.2); after is 0.28.0 (#2367, merged as eb9f3386):
base after
ec"xy"ho hello > out.txt (single line, non-empty) rc=2 rc=2 <-- pre-existing
ec"x<NL>y"ho hello > out.txt (multi-line, non-empty) rc=0 rc=2
ec""ho x > f (single line, empty span) rc=2 rc=2
ec"<NL>"ho x > f (multi-line, empty span) rc=0 rc=2
Bash's command word in each case:
| command |
bash runs |
guard reads |
ec"xy"ho hello > out.txt |
ecxyho |
echo — false positive |
ec"x<NL>y"ho hello > out.txt |
ecx<newline>yho |
echo — false positive |
ec""ho x > f |
echo |
echo — correct |
ec"<NL>"ho x > f |
echo |
echo — correct |
The discriminator is whether the dropped span was empty. An empty span really does vanish in
bash, so splicing is right; a non-empty one is literal content of the word, so splicing is wrong.
strip_literals does not distinguish them outside a redirect operand — inside one it already does,
via the \x03 OPAQUE mark (#2226 / #2287).
Where it comes from
The mark-based scheme from #2287 is gated on _in_redirect_operand, deliberately, so that
normalize_segments, _producer_head, _cat_redir and the whitespace trims stayed byte-for-byte
unchanged. Command-word spans were therefore left on the unmarked path. #2367 then made the
multi-line join empty (to stop a physical newline splitting a producer from its own redirect), which
brought the multi-line spelling into agreement with the single-line one — including on this defect.
Suggested direction, not prescribed
A coherent fix marks a non-empty dropped span as opaque wherever it occurs in a command word, for
both line shapes, so that:
ec"xy"ho hello > out.txt and ec"x<NL>y"ho hello > out.txt stop blocking (they are not echo);
ec""ho x > f and ec"<NL>"ho x > f keep blocking (they are echo);
printf 'a<NL>b' > notes.md keeps blocking — the span there is an argument, not part of the
command word, and _producer_head matches printf before it.
Marking only the newline join, which is the narrower reading, would fix the multi-line half and put
the two spellings back in disagreement while losing the empty-span cases. That option should be
rejected explicitly rather than by omission.
Blast radius is strip_literals' unmarked-drop path, which every lane of this guard reads, so this
wants the same before/after direction count and floor set that #2287 and #2367 used.
Severity and provenance
Severity: LOW. It is a false positive (friction), not a bypass — the guard blocks something it
should allow, and the shape (ec"xy"ho) is structurally unusual. Filed because the guard's
producer-scoping is a stated contract and this is a real, measured departure from it.
Provenance: the symptom was raised by the automated reviewer on #2367
(#2367 (comment),
block-hook-bypass.sh:376), after that PR had merged. Its stated mechanism — "the empty join
reconstructs echo, so mark the newline opaque" — is incomplete: the four-row table above shows
the single-line form already did this on main, so the join is not the cause and marking it is not
the fix. Corrected mechanism recorded here so a future fix is not written against the wrong one.
Verified against 4c90b454 and eb9f3386.
Summary
strip_literalsinplugins/guardrails/hooks/block-hook-bypass.shdrops a quoted span's contentwithout leaving any mark when the span is not a redirect operand. Where that span sits inside a
command word, the two sides of the word are spliced together and can form a word bash never forms —
including the producer names
_producer_headmatches, so a command bash would run asecxyhoblocks as an
echo … > filewrite.This is a pre-existing defect, not a regression: measured below, the single-line spelling has
been
rc=2since well before the change that surfaced it.Evidence
Hook invoked as a decision function on
PreToolUseBash payloads (rc=2blocked,rc=0allowed).baseis4c90b454(guardrails 0.27.2);afteris 0.28.0 (#2367, merged aseb9f3386):Bash's command word in each case:
ec"xy"ho hello > out.txtecxyhoecho— false positiveec"x<NL>y"ho hello > out.txtecx<newline>yhoecho— false positiveec""ho x > fechoecho— correctec"<NL>"ho x > fechoecho— correctThe discriminator is whether the dropped span was empty. An empty span really does vanish in
bash, so splicing is right; a non-empty one is literal content of the word, so splicing is wrong.
strip_literalsdoes not distinguish them outside a redirect operand — inside one it already does,via the
\x03OPAQUE mark (#2226 / #2287).Where it comes from
The mark-based scheme from #2287 is gated on
_in_redirect_operand, deliberately, so thatnormalize_segments,_producer_head,_cat_redirand the whitespace trims stayed byte-for-byteunchanged. Command-word spans were therefore left on the unmarked path. #2367 then made the
multi-line join empty (to stop a physical newline splitting a producer from its own redirect), which
brought the multi-line spelling into agreement with the single-line one — including on this defect.
Suggested direction, not prescribed
A coherent fix marks a non-empty dropped span as opaque wherever it occurs in a command word, for
both line shapes, so that:
ec"xy"ho hello > out.txtandec"x<NL>y"ho hello > out.txtstop blocking (they are notecho);ec""ho x > fandec"<NL>"ho x > fkeep blocking (they areecho);printf 'a<NL>b' > notes.mdkeeps blocking — the span there is an argument, not part of thecommand word, and
_producer_headmatchesprintfbefore it.Marking only the newline join, which is the narrower reading, would fix the multi-line half and put
the two spellings back in disagreement while losing the empty-span cases. That option should be
rejected explicitly rather than by omission.
Blast radius is
strip_literals' unmarked-drop path, which every lane of this guard reads, so thiswants the same before/after direction count and floor set that #2287 and #2367 used.
Severity and provenance
Severity: LOW. It is a false positive (friction), not a bypass — the guard blocks something it
should allow, and the shape (
ec"xy"ho) is structurally unusual. Filed because the guard'sproducer-scoping is a stated contract and this is a real, measured departure from it.
Provenance: the symptom was raised by the automated reviewer on #2367
(#2367 (comment),
block-hook-bypass.sh:376), after that PR had merged. Its stated mechanism — "the empty joinreconstructs
echo, so mark the newline opaque" — is incomplete: the four-row table above showsthe single-line form already did this on
main, so the join is not the cause and marking it is notthe fix. Corrected mechanism recorded here so a future fix is not written against the wrong one.
Verified against
4c90b454andeb9f3386.