[BUG] "Stop hook error" displayed despite hooks producing zero output (v2.0.28)

Resolved 💬 18 comments Opened Oct 27, 2025 by terrylica Closed Jan 15, 2026

[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:

  1. Produce 0 bytes of output (verified)
  2. Exit with code 0
  3. Have proper output redirection (> /dev/null 2>&1 &)
  4. 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)

  1. Added block-level output redirection to all background processes

``bash
} > /dev/null 2>&1 &
``

  1. Suppressed tool debug output (environment variables)

``bash
export UV_NO_PROGRESS=1
export RUST_LOG=error
``

  1. Redirected all subprocess stdout/stderr

``bash
subprocess_command >> /dev/null 2>> "$log_file"
``

  1. Added explicit exit 0 to all hooks
  1. Killed stale cached processes (24+ hour old bash processes)
  1. Implemented single-instance protection (prevents duplicate processes)
  1. Added stale process cleanup (auto-kills processes >1 hour old)
  1. 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

  1. If hooks produce 0 bytes output, do not show "Stop hook error"
  2. Clarify documentation: Are Stop hooks required to output JSON?
  3. Fix detection logic: "Hook output does not start with {" should not trigger for empty output
  4. Alternative: If JSON is required, provide clear error message: "Stop hooks must output JSON starting with { or produce no output"

Questions for Claude Team

  1. Are Stop hooks required to output JSON starting with {?
  2. Should empty output (0 bytes, exit 0) trigger "Stop hook error"?
  3. Is this related to the v2.0.17+ hook display regression (#9602)?
  4. 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)

View original on GitHub ↗

19 Comments

ahmedelgabri · 8 months ago

@terrylica did you manage to find a solution around this?

I'm hitting the same exact problem with UserPromptSubmit but it's even more odd, because I have two scripts, one runs fine the other doesn't.

"UserPromptSubmit": [
	{
		"hooks": [
			{
				"type": "command",
				"command": "~/.claude/hooks/log-event.sh UserPromptSubmit"
			},
			{
				"type": "command",
				"command": "~/.claude/hooks/aggregate-prompts.sh UserPromptSubmit"
			}
		]
	}
],

log-event.sh runs but aggregate-prompts.sh doesn't and I'm on 2.0.34

Even if I switch the order aggregate-prompt.sh never runs but log-event.sh and I see UserPromptSubmit hook error in the TUI.

No matter what, even if I have an empty script with exit 0

kevinwei0410 · 7 months ago

Hi @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!

ahmedelgabri · 7 months ago

@kevinwei0410, unfortunately and frustratingly, I didn't find any solution or workaround.

kitaekatt · 7 months ago

Additional Findings: PreToolUse Hooks + stdout_len: 0

Adding diagnostic information from investigating this issue with PreToolUse hooks using the cchooks Python SDK.

Environment

  • Claude Code: v2.0.58 (latest as of Dec 10, 2025)
  • Platform: Linux (Ubuntu 24.04)
  • Hook implementation: Python with cchooks SDK (context.output.allow())

Symptoms

● Bash(grep -c ...)
  ⎿  PreToolUse:Bash hook error
  ⎿  PreToolUse:Bash hook error
  ⎿  0

Two "hook error" lines appear because two PreToolUse hooks run. The command executes successfully.

Key Finding: stdout_len: 0 Despite SDK Output

Our hook logging captures full execution details. Here's what we see for successful hooks:

{
  "hook_name": "01-gated_tool_access_control.py",
  "execution": {
    "exit_code": 0,
    "success": true
  },
  "hook_output": {
    "decision": "allow",
    "reason": "Required skill 'tool-access' is invoked"
  }
}

However, checking stdout length shows:

{"hook": "01-gated_tool_access_control.py", "stderr": null, "stdout_len": 0}
{"hook": "gate_sequence_commands.py", "stderr": null, "stdout_len": 0}

The hooks use context.output.allow() from the cchooks SDK, which should output valid JSON to stdout. But stdout_len: 0 suggests 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:

  1. Buffering issue - Python stdout not flushed before Claude Code reads
  2. Race condition - Claude Code reads stdout before hook finishes writing
  3. SDK issue - cchooks not writing to stdout in a way Claude Code captures

Why This Matters

  • Affects PreToolUse hooks (not just Stop/SessionStart as previously reported)
  • Occurs with structured SDK usage, not just manual bash scripts
  • The stdout_len: 0 observation suggests a different root cause than JSON parsing
  • Multiple hooks per tool invocation = multiple spurious "error" lines (UX impact compounds)

Reproduction

Any PreToolUse hook using the cchooks SDK pattern:

from cchooks import create_context, PreToolUseContext

def main():
    context = create_context(PreToolUseContext)
    # ... validation logic ...
    context.output.allow(reason="Tool access permitted")

if __name__ == "__main__":
    main()

Happy to provide more details or test diagnostic builds if helpful.

kitaekatt · 7 months ago

Update: stdout flush workaround tested - doesn't help

Tested adding sys.stdout.flush() after context.output.allow() calls. Result: No change - "hook error" still displayed.

Direct Hook Testing

Tested hooks directly via stdin pipe:

$ echo '{"tool_name": "Bash", ...}' | ./gate_sequence_commands.py 2>/dev/null
{"continue": true, "suppressOutput": false, "hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": ""}}
$ echo '{"tool_name": "Bash", ...}' | ./gate_sequence_commands.py 2>/dev/null | xxd | head -1
00000000: 7b22 636f 6e74 696e 7565 223a 2074 7275  {"continue": tru

Findings

  1. Hooks output valid JSON to stdout ✓
  2. JSON starts with { (hex 7b) - no BOM, no whitespace, no hidden characters ✓
  3. No stderr output during normal execution ✓
  4. sys.stdout.flush() doesn't help - the issue isn't Python buffering

Conclusion

The bug is in Claude Code's hook handler, not in hook implementations. Hooks are:

  • Exiting with code 0
  • Outputting valid JSON starting with {
  • Not writing to stderr

Yet Claude Code displays "hook error". This rules out:

  • ❌ Python stdout buffering
  • ❌ Hidden characters before JSON
  • ❌ stderr pollution

The issue must be in how Claude Code spawns/reads from hook subprocesses.

tainora · 7 months ago

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:

  • Use } > /dev/null 2>&1 & instead of } &
  • Add explicit exit 0 at end of script
  • Ensure nested scripts also suppress output

Example (defense-in-depth pattern):

{
    # Background work here
    some_command > /dev/null 2>&1
} > /dev/null 2>&1 &  # Block-level redirection catches any leaks

exit 0  # Explicit exit before async output appears

Different Failure Mode (@kitaekatt's case)

Your diagnostic proves a DIFFERENT bug exists:

  • Hooks output valid JSON (verified with hex dump)
  • No format issues, BOM, or hidden characters
  • Bug appears to be in Claude Code's subprocess reader

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:

  • #9602 - Hook fires once but UI shows 4-7x duplicates
  • #9679 - SessionStart hook message repeats every response
  • #12667 - "error" displayed for intentional {"decision": "block"}
  • #13912 - UserPromptSubmit stdout causes error (docs say it shouldn't)
  • #11224 - PostToolUse visibility depends on exit code + stream combo

Execution Failures:

  • #10401 - All hooks require --debug flag (v2.0.27)
  • #10450 - No hooks work on Windows
  • #11716 - Background processes cause 11.6x token waste

Regression Pattern:

  • v2.0.15: Hooks worked correctly ✅
  • v2.0.17+: Regressions introduced

Request for Anthropic Team

  1. Documentation: What is the expected hook output contract? (JSON vs plain text vs zero output)
  2. Visibility rules: How do exit codes (0, 1, 2) and streams (stdout, stderr) affect what Claude sees?
  3. Size limits: Are there undocumented size limits or character filters on hook output?
  4. Background processes: Best practices for async/background work in hooks?

Would be great to have official hook authoring guidelines in the docs.

cc: @ahmedelgabri @kevinwei0410 @kitaekatt

davidmontemayort · 7 months ago

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:

=== Hook started at 1765953992.9169059 ===
Input received: {'session_id': '...', 'source': 'resume', ...}
Source: resume
SUCCESS: Completed in 0.086s, output size: 3001 chars

Key metrics:

  • Execution time: 86ms (well within any timeout)
  • Output: Valid JSON with hookSpecificOutput.additionalContext
  • Exit code: 0 (success)

Attempted 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:

  • Executes successfully in <100ms
  • Outputs valid JSON
  • Returns exit code 0

This bug affects multiple hook types (Stop, SessionStart, UserPromptSubmit) with the same pattern: successful execution shown as error in UI.

lassiter · 6 months ago
⏺ Ran 1 stop hook
  ⎿  Stop hook error: Failed with non-blocking status code: No stderr output

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.

wenzhuo4657 · 6 months ago

I also had this issue, the bat script ran successfully, but ended up prompting this error

wenzhuo4657 · 6 months ago

What's even more outrageous is that the hook content doesn't take effect either

kitaekatt · 6 months ago

Thanks appreciate it will validate when live

terrylica · 6 months ago

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:

[DEBUG] Hook output does not start with {, treating as plain text

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 Sees reason? |
|-----------|---------------------------|----------------------|
| 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: PostToolUse only fires on successful tool completion. Failed Bash commands (exit ≠ 0) do NOT trigger PostToolUse.

4. PostToolUse Visibility Requires decision: "block"
# ❌ WRONG - Claude sees NOTHING
echo '{"reason": "Please fix this"}'

# ✅ CORRECT - Claude sees the reason
jq -n --arg reason "Please fix this" '{decision: "block", reason: $reason}'

---

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

Stefan2142 · 6 months ago

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

jbeckton · 5 months ago

I don't even have any hooks configured and I see the startup hook error all the time.

SessionStart:startup hook error

Unless this is caused by one of the few plugins I have.

ryletko · 5 months ago
I don't even have any hooks configured and I see the startup hook error all the time. SessionStart:startup hook error Unless 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.

Stefan2142 · 5 months ago
Im having still an issue with "PostToolUse"..it basically this one #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

This has been fixed as of the latest version of 2026 Jan 19 (2.2.12)

jbeckton · 5 months ago
> I don't even have any hooks configured and I see the startup hook error all the time. > SessionStart:startup hook error > Unless 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.

Same, my issue was in the superpowers plugin

kitaekatt · 5 months ago
> I see the error "SessionStart:startup hook error" at startup after installation of superpowers plugin. No error when the plugin is disabled. 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.

github-actions[bot] · 5 months ago

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.