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.
Stop hook error: JSON validation failed
Stop hook error: Failed with non-blocking status
spawn /bin/sh ENOENT
invalid_type — expected: boolean, received: undefinedHook 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 event | Unknown fields in your output |
|---|---|
| Most events | Silently ignored |
| Stop | Rejected — 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
{"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:
claude --help"invalid_type — expected boolean, received undefined"#
#11947 reports a prompt-based Stop hook that cannot produce acceptable JSON, failing with:
"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:
./your-hook-script.sh | python -m json.toolIf that errors, the problem is your script, not Claude Code.
spawn /bin/sh ENOENT#
#21162 — Stop hooks failing at session end with:
spawn /bin/sh ENOENTENOENT 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/shdoesn'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/shmay genuinely be absent. Check:
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#
# 1. does the script produce valid JSON on its own?
./your-hook-script.sh | python -m json.tool# 2. can the shell it needs actually be found?
ls -l /bin/sh# 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.