.claude/hooks/block-dangerous-git.sh fails open: when its JSON parser is unavailable, a command on its own denylist passes through and the hook reports success.
The dependency
Line 4 parses the hook payload with jq:
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
jq is not declared as a requirement anywhere in the repository. On a machine without it — ours was one — the substitution yields an empty string, no pattern matches, and the script reaches its final exit 0.
Measured, both directions
Revision 3d6c941d52. With jq removed from PATH, feeding it a command from its own denylist:
hooks/block-dangerous-git.sh: line 4: jq: command not found
exit status 0
Zero means allowed. With jq present, the same input behaves correctly:
dangerous input -> exit 2, "BLOCKED: ... matches dangerous pattern ..."
ordinary input -> exit 0, silent
So the matching logic is sound; the failure mode is not. The only way this guard can fail is by silently permitting.
The fix worth making
A parser being unavailable is not evidence that a command is safe. If the payload cannot be parsed, the hook should exit 2 with a clear message rather than 0. That matters more than the jq dependency itself: with jq present, any other parse failure still yields the same silent permission.
Worth noting what this guard protects. Its denylist includes git reset --hard, which is exactly the class of command that destroys uncommitted work belonging to someone else. A guard for that should not have "allow everything" as its failure mode.
Lower priority, but a real cost
Matching is a substring test over the whole command with no notion of data versus execution, so writing about these commands is blocked too. We were unable to send an incident report through the normal path twice, because the report quoted one of the patterns inside a heredoc. It errs in the safe direction, but it means the guard cannot be documented by an agent working under it.
.claude/hooks/block-dangerous-git.shfails open: when its JSON parser is unavailable, a command on its own denylist passes through and the hook reports success.The dependency
Line 4 parses the hook payload with
jq:jqis not declared as a requirement anywhere in the repository. On a machine without it — ours was one — the substitution yields an empty string, no pattern matches, and the script reaches its finalexit 0.Measured, both directions
Revision
3d6c941d52. Withjqremoved fromPATH, feeding it a command from its own denylist:Zero means allowed. With
jqpresent, the same input behaves correctly:So the matching logic is sound; the failure mode is not. The only way this guard can fail is by silently permitting.
The fix worth making
A parser being unavailable is not evidence that a command is safe. If the payload cannot be parsed, the hook should exit 2 with a clear message rather than 0. That matters more than the
jqdependency itself: withjqpresent, any other parse failure still yields the same silent permission.Worth noting what this guard protects. Its denylist includes
git reset --hard, which is exactly the class of command that destroys uncommitted work belonging to someone else. A guard for that should not have "allow everything" as its failure mode.Lower priority, but a real cost
Matching is a substring test over the whole command with no notion of data versus execution, so writing about these commands is blocked too. We were unable to send an incident report through the normal path twice, because the report quoted one of the patterns inside a heredoc. It errs in the safe direction, but it means the guard cannot be documented by an agent working under it.