Claude Code hook errors

Stop hooks validate their output strictly while every other hook event ignores unknown fields. That asymmetry causes most "JSON validation failed" reports.

Last reviewed · verified against the pingdotgg/t3code repo

text
Stop hook error: JSON validation failed
Stop hook error: Failed with non-blocking status
spawn /bin/sh ENOENT
invalid_type — expected: boolean, received: undefined

Hook failures cluster around one event. Stop hooks break far more often than the others, and the reason is a rule that isn't obvious.

The asymmetry behind most reports#

Every hook event returns JSON. But they don't validate it the same way:

Hook eventUnknown fields in your output
Most eventsSilently ignored
StopRejected — validation fails

So a hook script that works everywhere else fails on Stop, with no change to your code. This is documented in a downstream report against claude-mem: the adapter returned

json
{"continue": true, "suppressOutput": true}

Those keys are not part of Claude Code's Stop hook schema. Other events shrug them off; Stop rejects the whole payload.

The fix is to emit only the fields Stop actually defines — and to check the schema for your installed version rather than copying an example from another hook event:

bash
claude --help

"invalid_type — expected boolean, received undefined"#

#11947 reports a prompt-based Stop hook that cannot produce acceptable JSON, failing with:

text
"invalid_type", "expected": "boolean", "received": "undefined"

Read it literally: a required boolean field is missing. undefined means the key wasn't present at all, not that its value was wrong.

Two things commonly cause it:

A prompt-based hook returning prose. If the hook asks a model to produce the JSON, it may return an explanation, or fenced markdown, or valid-looking JSON missing a required key. Validation sees no boolean and fails.

A wrapper object. #31278 documents a plugin whose hooks.json had a nested hooks wrapper and a missing matcher field — structurally invalid before the hook ever ran.

Validate your hook's output as plain JSON before wiring it up:

bash
./your-hook-script.sh | python -m json.tool

If that errors, the problem is your script, not Claude Code.

spawn /bin/sh ENOENT#

#21162 — Stop hooks failing at session end with:

text
spawn /bin/sh ENOENT

ENOENT means the shell itself wasn't found, so the hook never executed. This is an environment problem, not a hook-logic problem.

  • On Windows, /bin/sh doesn't exist. A hook written for a POSIX shell needs Git Bash or WSL, and the same shell-resolution trouble behind exit code 1 applies here.
  • In a minimal container, /bin/sh may genuinely be absent. Check:
bash
ls -l /bin/sh
  • At session end specifically, the environment can be more restricted than during the session, so a hook that worked mid-session can still fail on Stop.

"Hook Error" on hooks that succeeded#

#10936 reports the status label showing "Hook Error" for hooks that actually ran successfully.

Worth knowing before you debug something that isn't broken: confirm the hook's side effect actually happened — the file it writes, the command it runs — rather than trusting the label.

Before you spend an evening on this#

#37559 argues that the hook documentation is misleading: Stop hooks broken, prompt hooks unable to inject context, and capabilities undocumented per event type. It notes related reports — #11947, #11786, #11610 — still unresolved as of v2.1.81. #2814 tracks hook system issues more broadly.

Debugging order#

bash
# 1. does the script produce valid JSON on its own?
./your-hook-script.sh | python -m json.tool
bash
# 2. can the shell it needs actually be found?
ls -l /bin/sh
bash
# 3. does it run outside Claude Code, with the same environment?
sh ./your-hook-script.sh; echo "exit: $?"

Then narrow by event: move the same hook to a non-Stop event. If it works there and fails on Stop, you've confirmed the strict-validation asymmetry, and the fix is in your output fields — not your logic.

FAQ#

Why does my Claude Code Stop hook fail JSON validation?#

Stop hooks validate their output strictly and reject unknown fields, while other hook events silently ignore them. A hook that works on other events can fail on Stop purely because it emits extra keys such as continue or suppressOutput that aren't in the Stop schema.

What does "invalid_type, expected boolean, received undefined" mean?#

A required boolean field is missing from your hook's JSON output. undefined means the key wasn't present at all. Prompt-based hooks often cause this by returning prose or fenced markdown instead of a bare JSON object.

How do I fix "spawn /bin/sh ENOENT" in a Stop hook?#

The shell couldn't be found, so the hook never ran. On Windows /bin/sh doesn't exist and the hook needs Git Bash or WSL; in a minimal container it may genuinely be missing. Check with ls -l /bin/sh.

Why does Claude Code show "Hook Error" when my hook worked?#

That's a reported display bug. Verify the hook's actual side effect rather than trusting the status label before you start debugging.

My hook runs but does nothing — is it broken?#

Possibly not. Hook capabilities differ per event and aren't fully documented, so the event you attached to may simply not be able to do what you're asking — injecting context from a prompt hook, for example.