Python hooks silently fail: stdout not flushed before pipe closes
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Python hooks that output JSON to stdout are silently ignored because Python buffers stdout when writing to a pipe. The hook runs, produces correct output, exits 0, but Claude Code reads an empty pipe.
Adding sys.stdout.flush() before exit fixes it, but this is a footgun that will hit every user writing hooks in Python.
What Should Happen?
Claude Code should either:
- Wait for the process to fully exit before reading stdout (which would include Python's atexit flush), or
- Document this requirement prominently in the hooks docs
Steps to Reproduce
- Create a Python hook script:
```python
#!/usr/bin/env python3
import json, sys
hook_input = json.load(sys.stdin)
command = hook_input.get("tool_input", {}).get("command", "")
if "dangerous_command" in command:
# This does NOT work — Claude Code sees empty stdout
print(json.dumps({"decision": "block", "reason": "blocked"}))
# This DOES work
# print(json.dumps({"decision": "block", "reason": "blocked"}))
# sys.stdout.flush()
```
- Register as a PreToolUse hook on Bash in settings.json
- Ask Claude to run a command containing "dangerous_command"
- Without flush: action proceeds silently (hook output ignored)
- With flush: action is correctly blocked
Additional issues discovered during debugging
- No hook identification in errors: When a hook errors, the UI shows generic "PreToolUse:Bash hook error" with no indication which of the N registered hooks failed. The hook command path should be included.
- "decision" field values undiscoverable: The docs show
hookSpecificOutput.permissionDecisionfor PreToolUse but{"decision": "block"}is what actually works for blocking. The relationship between these formats is unclear. We testedpermissionDecision: "deny"(documented) and it did nothing.{"decision": "block"}(undocumented for PreToolUse) is the only format that works.
- Hook errors are invisible to the AI: When a hook errors on a tool call, the error appears in the user's UI but is NOT included in the tool result returned to Claude. Claude confidently states "everything works" while the user sees 4 errors on every command. This makes hooks extremely difficult to debug from within a Claude session.
- Exit code 2 does not block: Despite docs saying exit 2 is a "blocking error", testing shows it produces a non-blocking warning and the action proceeds.
Claude Model
Opus
Is this a regression?
I don't know
Claude Code Version
2.1.92
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Xterm
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗