Filed by AI. Measured, not read — an earlier draft of this was withheld precisely because a
code-reading is not a verified negative, and readings have been wrong twice on this effort.
Two opposite failure postures in one guard
plugins/guardrails/hooks/block-dangerous-git.sh:
- Fails CLOSED on length.
MAX_COMMAND_LEN=16384 (:105); an over-ceiling command is treated as
obfuscation and blocked.
- Fails OPEN on missing
jq. hook::require_jq (hook-utils.sh:132) is
command -v jq >/dev/null 2>&1 && return 0, otherwise a once-per-session skip notice and
exit 0 — the whole hook is skipped and the tool call proceeds.
Measured, four cells
The two-cell version of this test cannot distinguish "the guard was skipped" from "the harness
returns ALLOW for everything," so the jq-present column is the discrimination control:
jq PRESENT jq HIDDEN
dangerous push DENY ALLOW
safe command ALLOW ALLOW
git push --force origin main is allowed when jq is unreachable. The jq-present column
proves the harness discriminates, so the ALLOW is the guard being skipped and not a stuck function.
Method: jq hidden via a BASH_ENV command() override that intercepts command -v jq and leaves
PATH untouched — stripping PATH directories would also remove git, which the guard needs, and
would produce the same ALLOW for an unrelated reason. The hook is a pure decision function (payloads
in, verdicts out), so nothing was executed and this is read-only by construction.
The length half was verified separately by the same shape: a safe command padded past 16384 is also
DENIED, which proves the denial is length-driven.
Why this is worth a design decision rather than a patch
Both postures are individually defensible and the file argues for each locally. Neither acknowledges
the other, and together they produce an odd threat model:
- An obfuscation-length command — a deliberate, adversarial act — is treated as hostile and blocked.
- An environment without
jq — an ordinary, undramatic state, and one a user is unlikely to
associate with a security guard at all — disables the guard entirely, silently after one
notice per session.
Someone who cannot get a dangerous command past 16384 characters can simply be somewhere without
jq. That is not a sophisticated bypass; it is the default state of a machine that has not installed
one dependency.
hook::require_jq is shared, so this posture is not local to this guard — it is the failure mode of
every hook that calls it, including block-convention-violation, block-hook-bypass, and
block-no-verify. The question is therefore worth answering once, at the helper.
Options, no recommendation implied
- Intended, leave it. The plugin's stated philosophy may be that an advisory guard must never
block work over a missing prerequisite. If so, this issue is closed by writing that reasoning next
to require_jq, where the two postures can be seen together.
- Fail closed for the irreversible-operation guards specifically, keeping fail-open for advisory
ones. Deny with a message naming jq as the missing prerequisite. Costs a hard dependency on a
tool the README already lists as required for other plugins.
- Degrade rather than skip. A jq-free substring pre-check for the highest-severity forms
(--force, -f, +refspec, --mirror) — weaker than the real parser, but not nothing. Risks a
false sense of coverage, which this repository has spent real effort learning to distrust.
Acceptance, whichever is chosen
- A test pinning the chosen posture with the four-cell shape above. Two cells cannot tell a skipped
guard from a broken harness.
- If the posture stays fail-open, the reasoning belongs at
hook::require_jq itself, not only at the
call sites — the call-site comments assert the behaviour, and nothing where the decision is made
explains why.
Related
Filed by AI. Measured, not read — an earlier draft of this was withheld precisely because a
code-reading is not a verified negative, and readings have been wrong twice on this effort.
Two opposite failure postures in one guard
plugins/guardrails/hooks/block-dangerous-git.sh:MAX_COMMAND_LEN=16384(:105); an over-ceiling command is treated asobfuscation and blocked.
jq.hook::require_jq(hook-utils.sh:132) iscommand -v jq >/dev/null 2>&1 && return 0, otherwise a once-per-session skip notice andexit 0— the whole hook is skipped and the tool call proceeds.Measured, four cells
The two-cell version of this test cannot distinguish "the guard was skipped" from "the harness
returns ALLOW for everything," so the
jq-present column is the discrimination control:git push --force origin mainis allowed whenjqis unreachable. Thejq-present columnproves the harness discriminates, so the ALLOW is the guard being skipped and not a stuck function.
Method:
jqhidden via aBASH_ENVcommand()override that interceptscommand -v jqand leavesPATHuntouched — strippingPATHdirectories would also removegit, which the guard needs, andwould produce the same ALLOW for an unrelated reason. The hook is a pure decision function (payloads
in, verdicts out), so nothing was executed and this is read-only by construction.
The length half was verified separately by the same shape: a safe command padded past 16384 is also
DENIED, which proves the denial is length-driven.
Why this is worth a design decision rather than a patch
Both postures are individually defensible and the file argues for each locally. Neither acknowledges
the other, and together they produce an odd threat model:
jq— an ordinary, undramatic state, and one a user is unlikely toassociate with a security guard at all — disables the guard entirely, silently after one
notice per session.
Someone who cannot get a dangerous command past 16384 characters can simply be somewhere without
jq. That is not a sophisticated bypass; it is the default state of a machine that has not installedone dependency.
hook::require_jqis shared, so this posture is not local to this guard — it is the failure mode ofevery hook that calls it, including
block-convention-violation,block-hook-bypass, andblock-no-verify. The question is therefore worth answering once, at the helper.Options, no recommendation implied
block work over a missing prerequisite. If so, this issue is closed by writing that reasoning next
to
require_jq, where the two postures can be seen together.ones. Deny with a message naming
jqas the missing prerequisite. Costs a hard dependency on atool the README already lists as required for other plugins.
(
--force,-f,+refspec,--mirror) — weaker than the real parser, but not nothing. Risks afalse sense of coverage, which this repository has spent real effort learning to distrust.
Acceptance, whichever is chosen
guard from a broken harness.
hook::require_jqitself, not only at thecall sites — the call-site comments assert the behaviour, and nothing where the decision is made
explains why.
Related