Observation noticed while fixing #10247 (PR #10406). Latent — I have no command shape where it produces a wrong verdict, which is why this is filed as an observation and not fixed in that PR.
The divergence
guard-main-checkout-bash.sh runs two independent quote-aware passes over the same text, and they treat a backslash outside quotes differently:
tokenize() has an explicit escape case — a backslash consumes the next character as literal content, so \" outside quotes is a literal " and does not open a quoted region. That matches a real shell.
split_segments() has no such case. Its case "$ch" in "'" | '"') q="$ch" fires on the " of a \", so it enters a quoted region a real shell would not enter.
PR #10406 fixes the inside-double-quote escape in both passes symmetrically. It deliberately leaves this outside-quotes asymmetry alone, because that one is not in the reported defect class and changing it moves the allow/block boundary.
Why it is worth recording
Segmentation decides where separators (; | & ( ) { } newline) split the command. If split_segments believes it is inside a quote when the shell would not be, it will not split there, and a write in a later segment can end up merged into a segment whose argv no longer parses as a write command. The failure direction is a missed write (fail-open), not a false block — consistent with the guard's stated philosophy, but silently rather than by choice.
Measured while probing #10406, this shape is blocked today and the block looks correct:
node -e \"j.x.map(st=>st.a).join(' - ')\"; -> BLOCK, target: st.a).join( - )"
That one is arguably right for the wrong reason: with \" being a literal character at shell level, the > in st=> really is unquoted, so a real shell would also treat it as a redirect. The concern is that the two passes reach that answer by disagreeing with each other, which makes the guard's behaviour on neighbouring shapes hard to reason about.
Suggested direction, if triage wants it
Give split_segments() the same backslash case tokenize() already has, then extend guard-main-checkout-bash.selftest.sh with both directions (a \" outside quotes that must still block a real redirect; one that must not fabricate a target). Anything that changes the allow/block boundary on a write guard deserves the negative twins, not just the allow probes — #10406's ablation showed this hook can regress in the block direction without anyone noticing.
Observation noticed while fixing #10247 (PR #10406). Latent — I have no command shape where it produces a wrong verdict, which is why this is filed as an observation and not fixed in that PR.
The divergence
guard-main-checkout-bash.shruns two independent quote-aware passes over the same text, and they treat a backslash outside quotes differently:tokenize()has an explicit escape case — a backslash consumes the next character as literal content, so\"outside quotes is a literal"and does not open a quoted region. That matches a real shell.split_segments()has no such case. Itscase "$ch" in "'" | '"') q="$ch"fires on the"of a\", so it enters a quoted region a real shell would not enter.PR #10406 fixes the inside-double-quote escape in both passes symmetrically. It deliberately leaves this outside-quotes asymmetry alone, because that one is not in the reported defect class and changing it moves the allow/block boundary.
Why it is worth recording
Segmentation decides where separators (
;|&(){}newline) split the command. Ifsplit_segmentsbelieves it is inside a quote when the shell would not be, it will not split there, and a write in a later segment can end up merged into a segment whose argv no longer parses as a write command. The failure direction is a missed write (fail-open), not a false block — consistent with the guard's stated philosophy, but silently rather than by choice.Measured while probing #10406, this shape is blocked today and the block looks correct:
That one is arguably right for the wrong reason: with
\"being a literal character at shell level, the>inst=>really is unquoted, so a real shell would also treat it as a redirect. The concern is that the two passes reach that answer by disagreeing with each other, which makes the guard's behaviour on neighbouring shapes hard to reason about.Suggested direction, if triage wants it
Give
split_segments()the same backslash casetokenize()already has, then extendguard-main-checkout-bash.selftest.shwith both directions (a\"outside quotes that must still block a real redirect; one that must not fabricate a target). Anything that changes the allow/block boundary on a write guard deserves the negative twins, not just the allow probes — #10406's ablation showed this hook can regress in the block direction without anyone noticing.