Skip to content

gate_git_diff hook refuses commands outside its scope: heredocs with apostrophes, $ in non-allow-listed git subcommands, long quoted input #537

Description

@Danathar

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.

  1. 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".
  2. 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.
  3. 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.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions