Found in a bug hunt on 2026-09-27 against 7250380 (main, v0.10.0). All payloads were piped to .claude/hooks/gate_git_diff.py as PreToolUse Bash input.
The hook's job is to make the Read(...) deny rules hold for the git diff/git log/git status allow rows and the gated :* rows. Several refusals fall outside that. A PreToolUse exit 2 blocks the call before the user can approve it at the normal permission prompt.
- Any command shlex can't tokenize is refused, even with no git in it.
refusal() tokenizes the whole command first. shlex doesn't understand heredocs, so an apostrophe in a quoted heredoc body raises ValueError and the command is refused with "its git arguments cannot be checked".
- The
$/redirection rules apply to every git subcommand, including ones no allow row covers (commit, checkout, show), which Claude Code would already prompt for. git commit -m "$(cat <<'EOF' … EOF)" is Claude Code's default commit form. The module docstring justifies these rules only by the allow rows ("every form above matches the allowed prefix, so none of them raises a prompt"). git commit -m 'plain' and gh pr create --body "$(cat <<'EOF' …)" pass.
- More than 131,072 quoted or escaped characters, or any character ≥ U+F0000, is refused.
hide() maps each quoted span to chr(0xF0000 + n), and only 131,072 such code points exist. The comment at .claude/hooks/gate_git_diff.py:1237-1239 doesn't mention the limit.
- The docstring says "Anything else exits 0" (
.claude/hooks/gate_git_diff.py:112-116), but the hook also exits 2 for shellcheck operands, unparseable input and unreadable hook JSON, all of which later sections describe.
Reproduction
"cat <<'EOF' > /tmp/x.txt\ndon't\nEOF" exit 2 Refused: the command cannot be parsed as shell words, so its git arguments cannot be checked.
"git add a && git commit -m \"$(cat <<'EOF'\nFix it\nEOF\n)\"" exit 2 Refused: $(cat <<'EOF' … ) carries a $ or a backtick that bash acts on …
"git checkout -b fix/$ISSUE" exit 2 Refused: fix/$ISSUE carries a $ or a backtick …
"git commit -m 'plain'" exit 0
"echo '<131071 × a>'" exit 2 Refused: the command cannot be parsed as shell words …
Expected behavior
A command with no git or gated-prefix invocation isn't refused because it can't be tokenized. The $ and redirection rules are scoped to the allow-listed git subcommands, so other git commands fall through to the normal permission prompt. Large quoted input isn't refused for a reason unrelated to its content. The docstring describes the exits the hook actually has.
Found in a bug hunt on 2026-09-27 against
7250380(main, v0.10.0). All payloads were piped to.claude/hooks/gate_git_diff.pyas PreToolUse Bash input.The hook's job is to make the
Read(...)deny rules hold for thegit diff/git log/git statusallow rows and the gated:*rows. Several refusals fall outside that. A PreToolUse exit 2 blocks the call before the user can approve it at the normal permission prompt.refusal()tokenizes the whole command first. shlex doesn't understand heredocs, so an apostrophe in a quoted heredoc body raises ValueError and the command is refused with "its git arguments cannot be checked".$/redirection rules apply to every git subcommand, including ones no allow row covers (commit,checkout,show), which Claude Code would already prompt for.git commit -m "$(cat <<'EOF' … EOF)"is Claude Code's default commit form. The module docstring justifies these rules only by the allow rows ("every form above matches the allowed prefix, so none of them raises a prompt").git commit -m 'plain'andgh pr create --body "$(cat <<'EOF' …)"pass.hide()maps each quoted span tochr(0xF0000 + n), and only 131,072 such code points exist. The comment at.claude/hooks/gate_git_diff.py:1237-1239doesn't mention the limit..claude/hooks/gate_git_diff.py:112-116), but the hook also exits 2 for shellcheck operands, unparseable input and unreadable hook JSON, all of which later sections describe.Reproduction
Expected behavior
A command with no git or gated-prefix invocation isn't refused because it can't be tokenized. The
$and redirection rules are scoped to the allow-listed git subcommands, so other git commands fall through to the normal permission prompt. Large quoted input isn't refused for a reason unrelated to its content. The docstring describes the exits the hook actually has.