Skip to content

guardrails: ps::blank_quoted_spans deletes quoted operands, so quoting evades the computed-call positional write signal #2906

Description

@kyle-sexton

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions