Claude Code

Claude Code "Stop hook error: JSON validation failed"

Last checked

The error

⏺ Ran 4 stop hooks
  ⎿  Stop hook error: JSON validation failed

PreToolUse hook error: Failed with non-blocking status code: /bin/sh: /path/to/hook.sh: No such file or directory

The first form is from issue #21992. The second follows the format in the hooks docs: <hook name> hook error, then the first line of stderr.

"<event> hook error" means one of your hooks failed in a non-blocking way, so Claude Code carried on without it. With JSON validation failed, the hook printed JSON that did not match that event's schema, or text mixed into its JSON. Pipe sample input into the script, make sure stdout contains only the JSON object, and read the full output with claude --debug.

Why it happens

Command hooks talk to Claude Code through stdout and exit codes. Stdout that starts with { and ends with } is parsed as JSON and checked against the event's schema; anything that fails is reported as a non-blocking hook error and the action goes ahead.

  1. Text before or around the JSON. An unconditional echo in ~/.zshrc or ~/.bashrc gets prepended to the hook's output (issue #21992, closed not planned). The docs now describe this under Hook JSON has no effect.
  2. Valid JSON with the wrong shape. Each event has its own fields: a Stop hook blocks with a top-level decision of block plus a reason, while PreToolUse puts permissionDecision inside hookSpecificOutput.
  3. Prompt-type Stop hooks. The model must answer with JSON such as ok, reason and optionally impossible; prose or other output fails validation (issue #41393, closed not planned).
  4. The built-in /goal Stop hook. Issue #62246 (closed 17 Aug 2026) showed this error on every /goal check. Issue #94057 (open) reports that a failed evaluator API call on long /goal sessions still shows only JSON validation failed.
  5. The script failed or never ran. A non-zero exit other than 2 or a missing, non-executable script shows Failed with non-blocking status code: and the first stderr line.

The fix

  1. 1 Test the hook by hand: echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh then echo $? to see the exit code.
  2. 2 Make stdout contain only the JSON object. Wrap profile output in if [[ $- == *i* ]]; then ... fi so it only runs in interactive shells.
  3. 3 Build the JSON with jq -n --arg (or Python or Node) instead of string concatenation, so quotes and backslashes are escaped.
  4. 4 Check the fields against the event's section in the hooks reference, for example {"decision": "block", "reason": "..."} for Stop.
  5. 5 Read the full output: start with claude --debug-file /tmp/claude.log and tail -f /tmp/claude.log, or run /debug mid-session. Press Ctrl+O for the transcript and /hooks to list what is loaded.
  6. 6 If the hook is a script, make it executable with chmod +x and reference it by absolute path or ${CLAUDE_PROJECT_DIR}.
claude --debug-file /tmp/claude.log

Exit codes in Claude Code hooks

Exit 0: no objection. Stdout is parsed as JSON if it looks like a JSON object; on most events it goes only to the debug log.

Exit 2: a blocking error. PreToolUse blocks the tool call, UserPromptSubmit rejects the prompt, and Stop keeps Claude working. Stderr is the reason unless your JSON gives one. JSON cannot override an exit 2 block.

Any other code, including 1: non-blocking for most events. The action proceeds and you see the hook error notice. If a hook is meant to enforce a policy, use exit 2, not exit 1.

Still failing?

  • Run claude --debug and read ~/.claude/debug/<session-id>.txt for the hook's exact stdout, stderr and exit code.
  • If the notice names no hook of yours and you use /goal, the error comes from the built-in evaluator, so update Claude Code and check issue #94057.
  • If a plugin's hook is the source, disable that plugin and see whether the notice stops.

Related errors

Hit a different error?

Paste any agent error and get the cause and fix in seconds.

Open the decoder

Frequently asked questions

Does a hook error stop Claude?

No. These are non-blocking errors: the action proceeds and the hook's output is ignored. Only exit code 2, or a valid blocking decision in JSON, blocks.

Why does my hook work in the terminal but fail in Claude Code?

Hooks run in a non-interactive shell that can still source your profile, and they get JSON on stdin. Test with piped sample input and check for extra lines printed before your JSON.

Where are hook logs?

In the debug log. Run claude --debug and read ~/.claude/debug/.txt, or use --debug-file to choose the path. --debug does not print to the terminal.

Stop firefighting agent errors

Decoding errors one at a time is the manual version of what BetterClaw automates. Run your agents on a no-code AI agent platform with managed models, retries and config validation built in.

Free plan available · Pro $49/mo · BYOK · 7-day money-back guarantee