Skip to content

docs(guardrails): block-dangerous-git's contract line says Bash-only; the matcher is Bash|PowerShell #2145

Description

@kyle-sexton

Filed by AI. A one-line documentation inaccuracy, filed because of what it cost rather than what it
is: two sessions independently investigated a suspected coverage gap that does not exist, on the
strength of this sentence.

The inaccuracy

plugins/guardrails/hooks/block-dangerous-git.sh opens with:

# PreToolUse hook: block irreversible git operations on Bash tool calls.

The same file contradicts it about fifteen lines later:

# the git guards are matched on both the Bash and the (opt-in) PowerShell tool,
# whose command arrives in the same tool_input.command field with PowerShell grammar.

And the wiring agrees with the second statement, not the first:

event=PreToolUse  matcher='Bash|PowerShell'
  cmd="${CLAUDE_PLUGIN_ROOT}"/hooks/block-dangerous-git.sh

The guard is not Bash-only. It reads .tool_name alongside .tool_input.command, dispatches subject
extraction on the tool name, and bundles a PowerShell grammar classifier
(lib/powershell/ps-command.sh) for the purpose.

Why a one-word fix is worth an issue

The opening line is the contract statement — the sentence anyone scoping work on this guard reads
first, and the one they will quote when deciding whether a surface is in scope. It is wrong, and it
is wrong in the direction that invents work: it implies an unguarded surface, so a reader concludes
there is an asymmetry to investigate. Two sessions did exactly that before measuring.

The measurement, for the record — identical command text under both tool names:

git push --force            Bash=DENY  PowerShell=DENY
git push -f                 Bash=DENY  PowerShell=DENY
git push origin +main       Bash=DENY  PowerShell=DENY
bare --force-with-lease     Bash=DENY  PowerShell=DENY
--no-force-if-includes      Bash=DENY  PowerShell=DENY
lease + --force-if-includes Bash=ALLOW PowerShell=ALLOW   (control)
safe push                   Bash=ALLOW PowerShell=ALLOW   (control)

Ten PowerShell-specific grammars — the call operator &, the stop-parsing token --%, semicolon and
&& chains, quoted executable paths, Start-Process -ArgumentList, single-quoted arguments, a
pwsh -Command wrapper, and an environment prefix — all DENY on both surfaces. The two ALLOW rows
are the discrimination control: the harness can return ALLOW, so the DENY rows are decisions rather
than a stuck function.

One form diverges, and it is correct: git push + backtick + newline + --force is a line
continuation in PowerShell (a real force push, denied) and an unterminated command substitution in
bash — bash -n reports `unexpected EOF while looking for matching ``. Bash cannot execute that
form at all, so the Bash-side ALLOW is inert rather than a hole. Two parsers each being right about
their own grammar is the intended outcome for a dual-grammar guard.

The fix

Reword the opening line to name both surfaces, matching the wiring and the statement fifteen lines
below it. Nothing else changes.

Untested, stated so the measurement is not over-read

  • The decision function only. Payloads in, verdicts out; no git push was executed, and whether
    a DENY verdict actually stops the tool call is the harness's contract rather than this script's.
  • hook::require_jq fails OPEN per the file's own header. The no-jq path was not tested on
    either surface, and a missing-jq environment is where a dual-surface guard would most plausibly
    diverge.
  • The command-length ceiling above which the command is not parsed. The threshold was not
    located or compared across grammars — a PowerShell command is typically longer than its bash
    equivalent for identical intent, so a shared ceiling is a plausible asymmetry and is untested.
  • ps-command.sh was probed black-box through ten forms, not read. Invoke-Expression/iex,
    splatting, aliases, and -ArgumentList with an array variable were not tried.
  • One machine, one git, one bash, and no pwsh was involved — PowerShell grammar semantics for
    the backtick were inferred from the classifier's behaviour and bash's refusal, not from running
    pwsh.

Related

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions