Hook processes hang forever: client never closes hook stdin pipe (Windows, 2.1.208+)
Claude Code bug report — hook stdin pipe left open → hook json.load(sys.stdin) blocks → session freezes
Reporter: Julian Hansen
Claude Code version: 2.1.211 (regression appeared ~2.1.208, around 2026-07-13)
Platform: Windows 11, Git Bash / MINGW64
Severity: High — intermittently freezes interactive sessions for minutes; makes the CLI feel unusable under a hook-heavy config.
Summary
Since upgrading into the 2.1.208–2.1.211 range (~3 days ago; the previous 12 months were stable),
sessions intermittently freeze mid-turn, immediately after a tool call. The freeze is not the
model — it is a synchronous hook that never returns because its stdin is never closed by the
client.
A hook reads its JSON payload with json.load(sys.stdin). Normally the client writes the payload
and closes the pipe, so the read returns. Intermittently (observed after Bash tool calls) the
client spawns the hook but never closes the hook's stdin pipe. json.load(sys.stdin) then
blocks forever. Because hooks run synchronously inside the session's turn, the whole session
hangs until something external kills the hook process — at which point the session resumes cleanly.
Killing the hung hook process (not the session) unfreezes the session. That single fact localises
the bug to the client's hook-stdin handling, not the model or the hook logic.
Reproduction (probabilistic)
- Configure a
PreToolUse/PostToolUsehook (matcherBashor.*) whose script does
json.load(sys.stdin).
- Run many
Bashtool calls in a session. The more hooks fire per tool call, the higher the hit
rate — in our config ~15 hook processes spawn per Bash call, and freezes occur every few
dozen calls.
- Occasionally a hook process stays alive with its stdin never receiving EOF; the session freezes
at that tool call.
Observed live: a worker session sat frozen 50 minutes on a git tool call, process alive but
~7% CPU, no turn progress, until externally reaped.
Expected vs actual
- Expected: the client always closes the hook's stdin after writing the payload (or the hook
read has a client-side deadline), so json.load(sys.stdin) always returns.
- Actual: stdin is sometimes left open; the hook blocks indefinitely; the session freezes for
the full duration until the hook is force-killed.
Impact
Any hook-based workflow is exposed. Users cannot tell a frozen session from a working one, and the
only recovery is to find and kill the specific hung child process. It has forced us to build three
layers of mitigation (a hung-hook reaper, a per-hook lifetime watchdog, and a session-level
kill+requeue reaper) — all reacting to the freeze after it happens rather than preventing it.
Requested fix
Close the hook's stdin pipe after writing the payload (send EOF), and/or apply the hook timeout
setting to the stdin read as a hard client-side deadline so a hook can never outlive its budget.
Local mitigations already in place (context, not a fix)
- Bounded stdin read helper (
read_stdin_json) instead of rawjson.load(sys.stdin). - A process-lifetime watchdog armed at hook import that
os._exit(0)s the hook after a timeout
(currently 120s — being lowered to ~15s so freezes self-clear in seconds).
- Note:
sqlite3.connect(timeout=N)does NOT bound query execution time (only lock-wait), so a
slow hook DB read against a large WAL is a second, separate freeze source we handle locally.
Still reproducible on 2.1.212; ~250 hang events documented in our reap logs; mitigations deployed fleet-side (bounded stdin reads + external reaper).