[BUG] "Stop hook error" displayed despite hooks producing zero output (v2.0.28)
[BUG] "Stop hook error" displayed despite hooks producing zero output (v2.0.28)
Summary
Claude Code v2.0.28 displays "⎿ Stop hook error" after every response, even though all Stop hooks produce exactly 0 bytes of output when tested manually. The error persists across fresh sessions despite comprehensive fixes including output redirection, process cleanup, and single-instance protection.
Environment
- Claude Code Version: 2.0.28
- Platform: macOS Darwin 24.6.0
- Terminal: iTerm.app / Ghostty
- Installation: npm global (
~/.claude/local/node_modules/.bin/claude) - Configuration:
~/.claude/settings.json
Bug Description
Observed Behavior
> Tell me a joke
⏺ Why do programmers prefer dark mode?
Because light attracts bugs! 🐛💡
⎿ Stop hook error <-- ERROR APPEARS EVERY TIME
Error appears:
- ✅ On every Stop event (end of Claude's response)
- ✅ Across fresh sessions (not cached)
- ✅ In multiple workspaces
- ✅ Even with --debug mode enabled
Debug log shows:
[DEBUG] Getting matching hook commands for Stop with query: undefined
[DEBUG] Found 1 hook matchers in settings
[DEBUG] Matched 3 unique hooks for query "no match query" (3 before deduplication)
[DEBUG] Hook output does not start with {, treating as plain text
[DEBUG] Hook output does not start with {, treating as plain text
[DEBUG] Hook output does not start with {, treating as plain text
Expected Behavior
Stop hooks should execute silently with no error message since they:
- Produce 0 bytes of output (verified)
- Exit with code 0
- Have proper output redirection (
> /dev/null 2>&1 &) - Are configured correctly per documentation
Actual Behavior
"Stop hook error" appears after every response, despite hooks functioning correctly (they execute, produce no output, and complete successfully).
Reproduction Steps
1. Configure Stop Hooks
~/.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "$HOME/.claude/automation/hook-1.sh"
},
{
"type": "command",
"command": "$HOME/.claude/automation/hook-2.sh"
},
{
"type": "command",
"command": "$HOME/.claude/automation/hook-3.sh"
}
]
}
]
}
}
2. Create Minimal Test Hook
/tmp/test-hook.sh:
#!/usr/bin/env bash
set -euo pipefail
# Background process with output redirection
{
# All operations redirected
echo "test" > /dev/null 2>&1
} > /dev/null 2>&1 &
# Exit immediately
exit 0
chmod +x /tmp/test-hook.sh
3. Verify Hook Produces Zero Output
$ echo '{}' | /tmp/test-hook.sh 2>&1 | wc -c
0
$ echo '{"session_id":"test","hook_event_name":"Stop"}' | /tmp/test-hook.sh 2>&1 | wc -c
0
4. Start Claude Code
$ claude --debug
> Tell me a joke
# ERROR APPEARS: "⎿ Stop hook error"
5. Check Debug Log
$ grep "Hook output" ~/.claude/debug/*.txt | tail -5
[DEBUG] Hook output does not start with {, treating as plain text
[DEBUG] Hook output does not start with {, treating as plain text
[DEBUG] Hook output does not start with {, treating as plain text
Verification Tests Conducted
Test 1: Manual Hook Execution
All hooks verified to produce 0 bytes when executed manually:
$ echo '{"session_id":"test","hook_event_name":"Stop"}' | ./hook.sh 2>&1 | wc -c
0
Test 2: Delayed Output Check
$ echo '{}' | ./hook.sh > /tmp/out 2>&1
$ sleep 5 # Wait for background processes
$ wc -c < /tmp/out
0
Test 3: Process Inspection
$ ps aux | grep hook | grep -v grep
# No stale processes found
Test 4: File Handle Check
$ lsof | grep hook
# No open file handles to old hook versions
Attempted Fixes (All Verified, None Resolved Issue)
- ✅ Added block-level output redirection to all background processes
``bash``
} > /dev/null 2>&1 &
- ✅ Suppressed tool debug output (environment variables)
``bash``
export UV_NO_PROGRESS=1
export RUST_LOG=error
- ✅ Redirected all subprocess stdout/stderr
``bash``
subprocess_command >> /dev/null 2>> "$log_file"
- ✅ Added explicit exit 0 to all hooks
- ✅ Killed stale cached processes (24+ hour old bash processes)
- ✅ Implemented single-instance protection (prevents duplicate processes)
- ✅ Added stale process cleanup (auto-kills processes >1 hour old)
- ✅ Verified no cached file descriptors (killed all hook processes, restarted fresh)
Root Cause Analysis
Theory 1: Claude Code UI Bug
Claude Code reports "Hook output does not start with {" even when hooks produce zero bytes of output. This may be:
- A regression from v2.0.17+ hook display issues (#9602)
- False positive detection
- Race condition in output capture
- Incorrect handling of empty output
Theory 2: JSON Output Expectation Mismatch
Error message "Hook output does not start with {" suggests Claude Code expects JSON output from Stop hooks, but documentation indicates:
- Hooks can output nothing (exit 0 = success, no output)
- JSON output is optional for structured responses
- Plain exit codes should be valid
Question: Are Stop hooks required to output JSON? If so, this should be documented. If not, empty output should not trigger an error.
Related Issues
- #9679 - Hook status messages displayed on every API response (v2.0.19-2.0.20)
- #9602 - Stop Hook Regression in 2.0.17-2.0.20 - Fires Multiple Times
- #10401 - All hooks require --debug flag in v2.0.27
- #9052 - Stop Hooks Causing Unexpected Processing State Interruption
Impact
- Severity: Medium (cosmetic but persistent)
- User Experience: Error messages clutter every interaction
- Functionality: Hooks work correctly despite error messages
- Workaround: None (error appears regardless of fixes)
Requested Fix
- If hooks produce 0 bytes output, do not show "Stop hook error"
- Clarify documentation: Are Stop hooks required to output JSON?
- Fix detection logic: "Hook output does not start with {" should not trigger for empty output
- Alternative: If JSON is required, provide clear error message: "Stop hooks must output JSON starting with { or produce no output"
Questions for Claude Team
- Are Stop hooks required to output JSON starting with
{? - Should empty output (0 bytes, exit 0) trigger "Stop hook error"?
- Is this related to the v2.0.17+ hook display regression (#9602)?
- Can you reproduce with minimal test hook shown above?
---
Report Date: 2025-10-27
Reproducibility: 100% (consistent across all sessions)
Blocker: No (hooks work, error is cosmetic)
Versions Affected: v2.0.28 (possibly earlier versions)
19 Comments
@terrylica did you manage to find a solution around this?
I'm hitting the same exact problem with
UserPromptSubmitbut it's even more odd, because I have two scripts, one runs fine the other doesn't.log-event.shruns butaggregate-prompts.shdoesn't and I'm on2.0.34Even if I switch the order
aggregate-prompt.shnever runs butlog-event.shand I seeUserPromptSubmit hook errorin the TUI.No matter what, even if I have an empty script with
exit 0Hi @ahmedelgabri @terrylica,
Excuse me, do you have any updates on this issue?
May I ask how you fixed it? I am also encountering the same problem now.
Thank you!
@kevinwei0410, unfortunately and frustratingly, I didn't find any solution or workaround.
Additional Findings: PreToolUse Hooks + stdout_len: 0
Adding diagnostic information from investigating this issue with PreToolUse hooks using the cchooks Python SDK.
Environment
context.output.allow())Symptoms
Two "hook error" lines appear because two PreToolUse hooks run. The command executes successfully.
Key Finding:
stdout_len: 0Despite SDK OutputOur hook logging captures full execution details. Here's what we see for successful hooks:
However, checking stdout length shows:
The hooks use
context.output.allow()from the cchooks SDK, which should output valid JSON to stdout. Butstdout_len: 0suggests Claude Code isn't receiving it.Hypothesis
This may not be "treating non-JSON as error" but rather "not receiving stdout at all". Possible causes:
Why This Matters
stdout_len: 0observation suggests a different root cause than JSON parsingReproduction
Any PreToolUse hook using the cchooks SDK pattern:
Happy to provide more details or test diagnostic builds if helpful.
Update: stdout flush workaround tested - doesn't help
Tested adding
sys.stdout.flush()aftercontext.output.allow()calls. Result: No change - "hook error" still displayed.Direct Hook Testing
Tested hooks directly via stdin pipe:
Findings
{(hex7b) - no BOM, no whitespace, no hidden characters ✓sys.stdout.flush()doesn't help - the issue isn't Python bufferingConclusion
The bug is in Claude Code's hook handler, not in hook implementations. Hooks are:
{Yet Claude Code displays "hook error". This rules out:
The issue must be in how Claude Code spawns/reads from hook subprocesses.
Update: Root Cause Analysis & Partial Workaround
After extensive investigation, I've identified what resolved MY case and discovered this is part of a larger issue affecting 14+ related GitHub issues.
My Resolution (Async Output Leak)
Root cause: Background blocks with
} &produce output AFTER the hook exits. Claude Code captures this delayed output and flags it as "hook error."Fix:
} > /dev/null 2>&1 &instead of} &exit 0at end of scriptExample (defense-in-depth pattern):
Different Failure Mode (@kitaekatt's case)
Your diagnostic proves a DIFFERENT bug exists:
My fix may not help your case - this seems to be a separate issue in how Claude Code reads hook subprocess output.
Related Issues (14+ Found)
Output Handling:
{"decision": "block"}Execution Failures:
--debugflag (v2.0.27)Regression Pattern:
Request for Anthropic Team
Would be great to have official hook authoring guidelines in the docs.
cc: @ahmedelgabri @kevinwei0410 @kitaekatt
Additional Case Report - SessionStart Hook
Claude Code Version: Latest (Dec 2025)
OS: macOS Darwin 24.6.0
Hook Type: SessionStart (also affects UserPromptSubmit)
Same bug pattern, different hook type. Adding diagnostic evidence:
Evidence
Added diagnostic logging to prove hook executes successfully:
Key metrics:
hookSpecificOutput.additionalContextAttempted Fixes (None Worked)
| Version | Change | Result |
|---------|--------|--------|
| v2.4.3 | Redirect stderr to /dev/null | Error persists |
| v2.4.4 | Increase timeout 5s→8s + memory cache | Error persists |
| v2.4.5 | Remove cache invalidation | Error persists |
| v2.4.6 | Disable auto_sync function | Error persists |
| v2.4.7 | Add diagnostic logging | Proved hook works correctly |
Conclusion
The "SessionStart:resume hook error" message is a false positive. The hook:
This bug affects multiple hook types (Stop, SessionStart, UserPromptSubmit) with the same pattern: successful execution shown as error in UI.
started showing up in 2.1.2 and on for me and has caused claude sessions to lockup at times and freeze my iTerm terminal.
I also had this issue, the bat script ran successfully, but ended up prompting this error
What's even more outrageous is that the hook content doesn't take effect either
Thanks appreciate it will validate when live
Update: Issue Resolved + Hook Patterns Reference
Current Status: No longer seeing "Stop hook error" on latest version. The fix appears to have been deployed after the Jan 12 report.
Root Cause (Retrospective)
The issue was Claude Code treating 0-byte output as a non-JSON error:
Empty output should be "success, no message" - not an error. This affected all hook types (Stop, SessionStart, UserPromptSubmit, PreToolUse).
---
Hook Patterns Reference (Lessons Learned)
While debugging this issue, I documented comprehensive hook patterns that may help others:
1. Hook Output Visibility by Type
| Hook Type |
decision: "block"Meaning | Claude Seesreason? ||-----------|---------------------------|----------------------|
| PostToolUse | Visibility only (tool already ran) | ✅ Yes, if present |
| Stop | ACTUALLY BLOCKS stopping | ✅ Yes, mandatory |
| SubagentStop | ACTUALLY BLOCKS subagent stopping | ✅ Yes, mandatory |
| UserPromptSubmit | Erases prompt | ❌ No (user only) |
| PreToolUse | Deprecated | ❌ Use
permissionDecision|2. Stop Hook Schema (Critical!)
| Intent | Output Format | Effect |
|--------|---------------|--------|
| Allow stop normally |
{}(empty object) | Claude stops normally || Block stop (continue) |
{"decision": "block", "reason": "..."}| Claude CANNOT stop || Informational message |
{"systemMessage": "..."}| Claude sees info, stops normally || Hard stop (emergency) |
{"continue": false, "stopReason": "..."}| Claude halted immediately |Common mistake: Using
{"continue": false}to "allow" stop - this actually means "HARD STOP Claude entirely" and causes:"Stop hook prevented continuation".3. PostToolUse Doesn't Fire on Errors
Critical limitation:
PostToolUseonly fires on successful tool completion. Failed Bash commands (exit ≠ 0) do NOT trigger PostToolUse.4. PostToolUse Visibility Requires
decision: "block"---
Full Reference
Comprehensive lifecycle reference with diagrams, visibility matrix, and templates:
https://github.com/terrylica/cc-skills/blob/main/plugins/itp-hooks/skills/hooks-development/references/lifecycle-reference.md
Im having still an issue with "PostToolUse"..it basically this one https://github.com/anthropics/claude-code/issues/15971. I was on latest version when this started happening. Tried reverting back one version and the issue was still present..that + flickering is really making it unusable. Plus if we add to the fact that now using of third party claude code cli clones that actually works is banned renders using of this totally impossible
I don't even have any hooks configured and I see the startup hook error all the time.
SessionStart:startup hook errorUnless this is caused by one of the few plugins I have.
I see the error "SessionStart:startup hook error" at startup after installation of superpowers plugin. No error when the plugin is disabled.
This has been fixed as of the latest version of 2026 Jan 19 (2.2.12)
Same, my issue was in the superpowers plugin
There definitely are issues with hook error logging and visibility. The CLI shows hook errors and Claude does not have visibility into them and they are not logged to any error log.
Expanding the hook error with ctrl+o often but not always shows more details as to the nature of the error.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.