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
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.shopens with:The same file contradicts it about fifteen lines later:
And the wiring agrees with the second statement, not the first:
The guard is not Bash-only. It reads
.tool_namealongside.tool_input.command, dispatches subjectextraction 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:
Ten PowerShell-specific grammars — the call operator
&, the stop-parsing token--%, semicolon and&&chains, quoted executable paths,Start-Process -ArgumentList, single-quoted arguments, apwsh -Commandwrapper, and an environment prefix — all DENY on both surfaces. The two ALLOW rowsare 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 +--forceis a linecontinuation in PowerShell (a real force push, denied) and an unterminated command substitution in
bash —
bash -nreports `unexpected EOF while looking for matching ``. Bash cannot execute thatform 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
git pushwas executed, and whethera DENY verdict actually stops the tool call is the harness's contract rather than this script's.
hook::require_jqfails OPEN per the file's own header. The no-jq path was not tested oneither surface, and a missing-jq environment is where a dual-surface guard would most plausibly
diverge.
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.shwas probed black-box through ten forms, not read.Invoke-Expression/iex,splatting, aliases, and
-ArgumentListwith an array variable were not tried.pwshwas involved — PowerShell grammar semantics forthe backtick were inferred from the classifier's behaviour and bash's refusal, not from running
pwsh.Related