PreToolUse hook: a valid single-document "allow" output is refused as a "suspect capture"
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:
- Before any verdict existed.
- The same, retried.
- After the plugin's own workflow returned
no_changesfor this HEAD. - After a second plugin had also written an
approveverdict 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 carryingadditionalContext:
{"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 -cwrapper aroundbash script.sh. The script spawns a detached subshell withnohup … >/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
Bashas well. The error names only this one. - One caveat on the manual reproduction: the hook input was hand-built, so
transcript_pathwas empty andtool_use_idsynthetic. The gate reads neither for its decision.