-
-
Notifications
You must be signed in to change notification settings - Fork 33k
PreToolUse:Bash dispatcher echoes input event to stdout → "Hook JSON output validation failed — (root): Invalid input" #2239
Description
Plugin version: 2.0.0
Symptom
On nearly every Bash tool call, Claude Code reports:
PreToolUse:Bash hook error
⎿ Hook JSON output validation failed — (root): Invalid input
Root cause
In scripts/hooks/bash-hook-dispatcher.js, runHooks() returns the unmodified raw stdin (the PreToolUse input event) on stdout whenever no sub-hook produced additionalContext:
return { output: additionalContext ? buildPreToolUseAdditionalContext(additionalContext) : currentRaw, // ← echoes the input event {session_id, hook_event_name, tool_name, tool_input, ...} ... };
Claude Code parses a hook's stdout as JSON and validates it against the hook output schema. The echoed input object (keys session_id, transcript_path, cwd, hook_event_name, tool_name, tool_input) doesn't match → (root): Invalid input.
It fires for any command where no sub-hook adds context (i.e. most commands). Commands that do add additionalContext produce a valid envelope and don't error, which is why it looks intermittent.
The early-return on a non-zero exit (return { output: currentRaw, ... }) has the same flaw. auto-tmux-dev has a related issue: it rewrites dev-server commands by emitting a full input-event JSON on stdout, which also cannot be valid hook output.
Reproduce
ROOT=~/.claude/plugins/cache/ecc/ecc/2.0.0 echo '{"session_id":"t","transcript_path":"/tmp/x","cwd":"'"$PWD"'","hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"ls -la"}}' \ | CLAUDE_PLUGIN_ROOT="$ROOT" node "$ROOT/scripts/hooks/pre-bash-dispatcher.js" # → prints the input event JSON back to stdout (should print nothing)
Fix
Only emit currentRaw when a sub-hook deliberately set stdout; otherwise emit ''. Track with a rawModified flag:
function runHooks(rawInput, hooks) { let currentRaw = rawInput; let rawModified = false; // ... const result = normalizeHookResult(currentRaw, hook.run(currentRaw)); if (result.raw !== currentRaw) rawModified = true; currentRaw = result.raw; // ... // non-zero early return: return { output: rawModified ? currentRaw : '', stderr, additionalContext, exitCode: result.exitCode }; // ... // final return: return { output: additionalContext ? buildPreToolUseAdditionalContext(additionalContext) : rawModified ? currentRaw : '', stderr, additionalContext, exitCode: 0, }; }
This preserves GateGuard's {stdout} pass-through and block-no-verify's exit-2 behavior, while emitting nothing in the common pass-through case. Verified locally: ls/git status/cat/echo → empty stdout + exit 0 (no error); --no-verify still blocks via exit 2 + stderr; additionalContext reminders still build a valid hookSpecificOutput envelope.
A PR with this change follows.