Python hooks silently fail: stdout not flushed before pipe closes

Resolved 💬 3 comments Opened Apr 5, 2026 by flound1129 Closed May 16, 2026

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:

  1. Wait for the process to fully exit before reading stdout (which would include Python's atexit flush), or
  2. Document this requirement prominently in the hooks docs

Steps to Reproduce

  1. 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()
```

  1. Register as a PreToolUse hook on Bash in settings.json
  2. Ask Claude to run a command containing "dangerous_command"
  3. Without flush: action proceeds silently (hook output ignored)
  4. With flush: action is correctly blocked

Additional issues discovered during debugging

  1. 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.
  1. "decision" field values undiscoverable: The docs show hookSpecificOutput.permissionDecision for PreToolUse but {"decision": "block"} is what actually works for blocking. The relationship between these formats is unclear. We tested permissionDecision: "deny" (documented) and it did nothing. {"decision": "block"} (undocumented for PreToolUse) is the only format that works.
  1. 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.
  1. 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

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗