Problem
ps::blank_quoted_spans (plugins/guardrails/lib/powershell/ps-command.sh:163) deletes quoted spans outright:
text=$(printf '%s' "$text" | sed -e "s/'[^']*'//g" -e 's/"[^"]*"//g')
Every probe downstream therefore sees a quoted operand as absent, not as present-but-opaque. For ps::computed_call_has_positional_write_signal, whose two-positional arm requires at least one visible literal operand, that means quoting an operand removes it from the count entirely — so quoting is a general evasion of the positional write signal.
Evidence
Executed against origin/main at 4e6249ae77335f8786b4ae36d0888073569c27a0 and against the pre-merge base 10bdd7c5127409fbe70026cec923d17e8bfd8c20, each command fed as a real PreToolUse PowerShell payload built by jq (no shell quoting touched the command string), rc from block-hook-bypass.sh:
| command |
base 10bdd7c5 |
main 4e6249ae7 |
& $w f.txt x (unquoted control) |
rc=2 |
rc=2 |
& $w 'f.txt' 'x' |
rc=0 |
rc=0 |
& $w 'f.txt' x |
rc=0 |
rc=0 |
& $w (Join-Path $d f.txt) 'x' |
rc=2 |
rc=0 |
The first three rows are rc=0 on both sides: this is a pre-existing residual, not something PR 2890 introduced. It is filed separately for exactly that reason.
The fourth row did change, but not because the quoting behavior changed. It blocked on base only because the blanket ps::has_special_constructs arm was catching the (/) grouping — the incidental over-block that issue 2848 exists to remove. Once that arm went, the row fell through to the positional probe and hit the same pre-existing quoted-operand blindness the rows above document. So the underlying defect is unchanged; PR 2890 only made one more shape reach it.
Why the current behavior is wrong
& $w 'f.txt' 'x' is a complete Set-Content <path> <value> call through a computed target. It is the exact Path+Value shape the #2722 positional signal exists to catch, and quoting the operands — the more idiomatic PowerShell spelling, not an obscure one — makes it invisible.
Suggested direction (not prescriptive)
Replace a quoted span with a placeholder token rather than deleting it, so the operand still counts as one visible literal while its content stays opaque. This must be scoped carefully: ps::blank_quoted_spans feeds many probes, and several depend on the deletion semantics (a quoted > or -value in message text must stay inert). A contained option is to derive a separate placeholder-preserving string inside ps::write_bypass and hand only that to ps::computed_call_has_positional_write_signal.
Over-block risk that must be measured before shipping
A placeholder makes quoted operands count, so & $py "script.py" "arg" would begin to block. That is consistent with today's & $py script.py arg (already rc=2), but it is a real behavior change and needs its own probe table across all three blocking hooks, plus the acceptance cases from issue 2848:
& $py $script (Join-Path $dir "$id.jsonl") must stay rc=0
$py = "..."; if (-not (Test-Path $py)) { ... }; & $py C:/s/run.py --flag must stay rc=0
& $py -m unittest discover, . $PROFILE, & $py script.py must stay rc=0
Provenance
Surfaced by the fresh-context verifier agent on PR 2890 (issue 2848) while adversarially probing merged content. The verifier's other finding — a braced call target (& ${env:w} …) matching no call site — was a regression from that PR and is fixed separately. This one is recorded as the pre-existing residual it is.
Problem
ps::blank_quoted_spans(plugins/guardrails/lib/powershell/ps-command.sh:163) deletes quoted spans outright:text=$(printf '%s' "$text" | sed -e "s/'[^']*'//g" -e 's/"[^"]*"//g')Every probe downstream therefore sees a quoted operand as absent, not as present-but-opaque. For
ps::computed_call_has_positional_write_signal, whose two-positional arm requires at least one visible literal operand, that means quoting an operand removes it from the count entirely — so quoting is a general evasion of the positional write signal.Evidence
Executed against
origin/mainat4e6249ae77335f8786b4ae36d0888073569c27a0and against the pre-merge base10bdd7c5127409fbe70026cec923d17e8bfd8c20, each command fed as a realPreToolUsePowerShellpayload built byjq(no shell quoting touched the command string), rc fromblock-hook-bypass.sh:10bdd7c54e6249ae7& $w f.txt x(unquoted control)& $w 'f.txt' 'x'& $w 'f.txt' x& $w (Join-Path $d f.txt) 'x'The first three rows are rc=0 on both sides: this is a pre-existing residual, not something PR 2890 introduced. It is filed separately for exactly that reason.
The fourth row did change, but not because the quoting behavior changed. It blocked on base only because the blanket
ps::has_special_constructsarm was catching the(/)grouping — the incidental over-block that issue 2848 exists to remove. Once that arm went, the row fell through to the positional probe and hit the same pre-existing quoted-operand blindness the rows above document. So the underlying defect is unchanged; PR 2890 only made one more shape reach it.Why the current behavior is wrong
& $w 'f.txt' 'x'is a completeSet-Content <path> <value>call through a computed target. It is the exact Path+Value shape the#2722positional signal exists to catch, and quoting the operands — the more idiomatic PowerShell spelling, not an obscure one — makes it invisible.Suggested direction (not prescriptive)
Replace a quoted span with a placeholder token rather than deleting it, so the operand still counts as one visible literal while its content stays opaque. This must be scoped carefully:
ps::blank_quoted_spansfeeds many probes, and several depend on the deletion semantics (a quoted>or-valuein message text must stay inert). A contained option is to derive a separate placeholder-preserving string insideps::write_bypassand hand only that tops::computed_call_has_positional_write_signal.Over-block risk that must be measured before shipping
A placeholder makes quoted operands count, so
& $py "script.py" "arg"would begin to block. That is consistent with today's& $py script.py arg(already rc=2), but it is a real behavior change and needs its own probe table across all three blocking hooks, plus the acceptance cases from issue 2848:& $py $script (Join-Path $dir "$id.jsonl")must stay rc=0$py = "..."; if (-not (Test-Path $py)) { ... }; & $py C:/s/run.py --flagmust stay rc=0& $py -m unittest discover,. $PROFILE,& $py script.pymust stay rc=0Provenance
Surfaced by the fresh-context verifier agent on PR 2890 (issue 2848) while adversarially probing merged content. The verifier's other finding — a braced call target (
& ${env:w} …) matching no call site — was a regression from that PR and is fixed separately. This one is recorded as the pre-existing residual it is.