PreToolUse hook: a valid single-document "allow" output is refused as a "suspect capture"

Status Open
Reported on v2.1.285
Maintainer reply None cached
Activity 0 comments · opened Sep 30, 2026

Environment

  • Claude Code 2.1.285, macOS 26.6.2 (Darwin 25.6.0), zsh
  • An internal plugin's PreToolUse hook, matcher Bash, timeout 120s

Command: bash -c 'g="${CLAUDE_PLUGIN_ROOT}/scripts/pre-push-gate.sh"; if [ -f "$g" ]; then bash "$g"; else echo "{}"; fi'

Symptom

Every Bash tool call running git push -u origin feature/my-branch fails before execution with:

PreToolUse:Bash hook error: [bash -c 'g=".../pre-push-gate.sh"; if [ -f "$g" ]; then bash "$g"; else echo "{}"; fi']:
hook output parsed as a document that grants, rewrites or injects, but its stdio went quiet before
end-of-stream and a dropped later document could have changed it; refusing to honor the suspect capture

It reproduced 4 times out of 4 in one session:

  1. Before any verdict existed.
  2. The same, retried.
  3. After the plugin's own workflow returned no_changes for this HEAD.
  4. After a second plugin had also written an approve verdict for this HEAD.

The error text is identical every time and the push never runs. Plugin state has no effect: the refusal happens in the capture step, before any hook decision is applied.

Diagnosis: the hook itself behaves correctly

The same script was run by hand with a hook input whose tool_input.command is byte-identical to the refused call:

{"session_id":"<redacted>","cwd":"<worktree>","permission_mode":"auto","hook_event_name":"PreToolUse",
 "tool_name":"Bash","tool_input":{"command":"git push -u origin feature/my-branch"},"tool_use_id":"manual-diag"}
  • Exit code: 0.
  • Timing: the pipe reader saw EOF on stdout after about 1s (bash gate.sh | { cat; date; }). Nothing keeps stdout open. The only background child is ( … nohup node … </dev/null >/dev/null 2>&1 & ) (gate script line 591).
  • Output: exactly ONE JSON document (jq -s length → 1). It is an allow/skip carrying additionalContext:
{"systemMessage":"gate skipped — no source files in this push (...)",
 "hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"gate skipped — ..."}}

So the hook produces one complete document and closes stdout promptly. The capture layer nonetheless reports that stdio "went quiet before end-of-stream" and refuses it. The document it refuses is an "injects" type (additionalContext), which matches the message's wording.

Expected

A single complete JSON document followed by EOF and exit 0 is honoured, and the tool call proceeds with the injected context.

Actual

The document is refused as a suspect partial capture and the tool call is blocked indefinitely — 4/4 attempts, independent of plugin state. The only way out was running the command outside the tool, which defeats the hook's purpose entirely.

Possibly relevant

  • The hook's stdout is a bash -c wrapper around bash script.sh. The script spawns a detached subshell with nohup … >/dev/null 2>&1 &. That child should not inherit the hook's stdout, but its stdio handling at the moment of detach may be what the capture layer interprets as "went quiet".
  • Other PreToolUse hooks are registered on Bash as well. The error names only this one.
  • One caveat on the manual reproduction: the hook input was hand-built, so transcript_path was empty and tool_use_id synthetic. The gate reads neither for its decision.

View original on GitHub ↗