Windows: hook processes are left SUSPENDED (never executed a single instruction), hanging the turn for 30-60+ minutes
Environment
- Claude Code on Windows 11 Pro (build 26200), Git Bash, PowerShell 5.1
- Hooks: Python scripts (
python.exe3.12), declared with"timeout": 2/"timeout": 3insettings.json
Summary
Claude Code sometimes leaves a hook process suspended and never resumed. The process never
executes a single instruction, so it never returns — and Claude Code waits on it, hanging the entire
turn for 30–60+ minutes. Escape does not cancel it; only restarting the session helps.
This is not a stdio/pipe deadlock and not a slow hook. The process is suspended, not blocked.
The signature (measured)
Live hook processes found alive 46–127 minutes despite a declared 2–3 s timeout:
| Property | Value |
|---|---|
| Thread count | 1 |
| Thread WaitReason | Suspended (kernel WrSuspended) |
| UserProcessorTime | 00:00:00 |
| Working set | ~1.9 MB |
| Parent process | dead |
Claude Code prints timed out - output discarded, but neither resumes nor kills the process.
Why this proves "never started", not "blocked on I/O"
WaitReason = Suspendedcannot be produced by a blocked read.ProcessThread.WaitReason
surfaces the kernel's KWAIT_REASON. A thread blocked in ReadFile on a pipe reports
UserRequest/Executive — never WrSuspended. These are distinct kernel codes.
- Zero CPU time really means zero instructions. Control experiment: 12
python.exeprocesses
that do start and then block on stdin were measured with GetProcessTimes after 3 s:
````
CPU times: [0.0312, 0.0312, 0.0312, 0.0312, 0.0156, 0.0312, ...]
processes reporting exactly 0.0 : 0 / 12 (min 0.0156, max 0.0312)
Python startup always crosses at least one 15.6 ms accounting quantum. So 0.0 is not a
granularity artifact — it discriminates "never ran" from "ran, then blocked".
- A single
ResumeThread()fixes it.OpenThread(THREAD_SUSPEND_RESUME)+ResumeThread()
returned previous suspend count = 1, and the process then completed its work and exited in
< 1 second (verified: it wrote its output file). Reproduced on 5 separate processes.
Since ResumeThread is a no-op returning 0 on a non-suspended thread, a return of 1 can only
mean the thread was genuinely suspended.
- The hook script is not at fault. Spawning the identical hook 120× via plain
subprocess.Popen (same stdin payload, same 3 s budget), outside Claude Code:
0 freezes, ~31 ms each. The freeze only occurs when Claude Code spawns the process.
- Windows Defender's Operational log contains zero events for
python.exe, and a plain
python -c pass spawn takes a stable ~65 ms — so AV scanning does not appear to be throttling
the spawn (though we cannot rule out AV interference at the kernel process-creation path).
Root cause: unknown
We deliberately do not claim a mechanism. Something either spawns these processes suspended or
calls SuspendThread on them and never resumes. For the record, we checked and ruled out the
obvious guess: in libuv/src/win/process.c, CREATE_SUSPENDED is set only underUV_PROCESS_DETACHED, and AssignProcessToJobObject sits in the mutually exclusive non-detached
branch — hooks are not detached, so that path is not it. We're reporting a reproducible signature and
a one-line remedy, and leaving root-cause to you.
Impact
- Turn hangs for tens of minutes on any tool (we observed it on a plain file
Read, not just Bash). Escapedoes not cancel the hung turn.- Silently corrupts hook semantics: a hook that never runs looks to Claude Code like a hook that
timed out. Side effects the user depends on (our session-edit manifest, guards, telemetry) simply
never happen.
Likely explains several open issues
This mechanism would account for reports that are currently filed separately:
- #34457 — Windows hook hangs (closed, not planned)
- #68970 — hooks fire at ~1.2% rate on Windows
- #76413 — PreToolUse hooks intermittently not invoked (Windows)
- #66151 — Stop hook orphan children (Windows)
- #67888 — Windows process-cleanup path hangs/accumulates
- #73546 — turn stuck on "running", timeout never fires
Suggested fixes
- On timeout, actually terminate the hook process. Today "output discarded" leaves it resident
forever. (See #67888 — the cleanup path itself is unreliable on Windows.)
- Resume before waiting / verify the child is runnable after spawn — a suspended child will
never produce output, so waiting on it is unbounded.
- Make
Escapecancel a turn that is blocked on a hook.
Workaround we are using
A watchdog that resumes (rather than kills) suspended hook processes: it polls every 5 s and
calls ResumeThread on hook processes with all threads suspended, zero CPU time, and age > 12 s.
The revived hook completes its work in < 1 s and exits, and the hung turn proceeds — no session
restart, no lost hook side effects.
Known limitation of the heuristic: a hook that legitimately blocks on a slow read also shows ~zero
CPU; only the age window protects it.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Still reproducing on 2.1.209 (latest) — 8 fresh freezes in a single day
Re-measured today (2026-07-14) on Claude Code 2.1.209, which is the current latest on npm. The bug is not fixed, and nothing in the CHANGELOG (all 4931 lines) mentions suspended hook processes.
New data
Eight hook processes were left suspended today alone, across four different hooks:
| Time (local) | Hook |
|---|---|
| 12:52 |
skill_telemetry_hook.py|| 12:53 |
outbound_audit_hook.py|| 13:09 |
session_edit_tracker.py|| 13:11 |
session_edit_tracker.py|| 14:38 |
tool_success_clear_hook.py|| 14:38 |
session_edit_tracker.py|| 14:39 |
session_edit_tracker.py|| 14:50 |
tool_success_clear_hook.py|Same signature as in the original report every time: thread
WaitReason = Suspended,UserProcessorTime = 00:00:00(the process never executed a single instruction),suspend_count = 1, declared hook timeout 2–3 s. A singleResumeThread()makes each one finish and exit in under a second.This is roughly 8 freezes over ~2 hours of active session work — it is not a rare edge case, it happens continuously.
Why the freezes no longer show up as hour-long hangs
I want to be explicit about this, so the absence of new "my session hung for 40 minutes" reports is not misread as the bug going away: I run an external watchdog that polls every 5 s and calls
ResumeThread()on any hook process that is suspended with zero CPU time past a grace window. Before the watchdog, these processes survived 825 s, 3348 s, 3691 s (13–61 minutes of a hung turn). With it, they are caught at 12–16 s.The freezes still happen at exactly the same rate. They just stop being painful. Without such a workaround, each of the eight events above would have hung a turn for tens of minutes, with
Escapeunable to cancel it.Related issues are closed, but none were fixed
For anyone triaging: #34457 and #66151 both show as CLOSED, but both were auto-closed by the stale bot (
NOT_PLANNED, "inactive for too long") — neither received a fix. #68970, #76413, #67888 and #73546 remain open. This cluster of Windows hook/process issues has been sitting untouched for months.Happy to run any instrumentation you want on a live repro — I can reproduce this at will and I still have the diagnostic scripts (
WaitReason/UserProcessorTimedump,ResumeThreadprobe) from the original report.Independent corroboration — same signature, byte for byte, plus some data points not yet in this thread.
Environment: Windows 11 Pro build 26200, Claude Code 2.1.220 (native install), Git Bash, Defender + EDR active. Hook children observed: CPython 3.12 scripts from installed plugins, Claude Code's own inline hook
if-condition evaluator, and one Node.js script.Live probe matching your table exactly
Three orphaned hook children sampled while sessions were wedged (re-sampled ~25 minutes later — unchanged; the oldest was still suspended ~2 h 47 m after spawn at last check):
| Property | Value (all three) |
|---|---|
| Thread count | 1 |
| Thread state /
WaitReason|Wait/Suspended||
UserProcessorTime| 00:00:00 ||
TotalProcessorTime| 00:00:00 || Working set | ~1.95 MB |
| Parent | dead |
| Ages at sampling | 141–167 min (declared hook timeouts: 5–15 s) |
Across 5 days on one endpoint: dozens of these, at 45 min ×2 (simultaneous), 62 min, 72 min, 128 min ×2 (simultaneous), ~12 h ×3 (spawned within 1 s of each other, sat overnight), and one at 13.5 h.
Data points that may extend the report
if-condition evaluator child gets suspended too (thepython3 -c "…os.environ['PATTERNS']…"process that evaluates hookif:gates). That reproduces the defect with no plugin code in the child at all.if-gated hooks that never fire still spawn the evaluator on every matching tool event — each spawn is a fresh exposure to the race.git checkout/git ls-remote/az …"Waiting…" for 2 h+, the identical commands run manually from another shell returned in under 2 s (no git locks, no credential prompts, network fine).claude -cafterward was fine — it's the process that matters), consistent with your report.One honest caveat: with EDR present I cannot locally distinguish "Claude Code created the child suspended and never resumed it" from "something external suspended it at spawn and was never able to resume it." But the signature, the multi-runtime spread, and the recovery behavior match this issue exactly — and AV/ASR blocks were separately verified to fail fast (
Access is denied) rather than hang.Ruled out locally before finding this issue (may save others the detour)
python3Store shim as the cause — suspended children include a real CPython 3.12 install and nodeAccess is denied; they never hang (verified by live probe)Happy to provide the full capture log (process tables, timeline, transcript excerpts with tool_use/tool_result pairing) if useful.
Still present on 2.1.259 (Windows 11 Pro build 26200, Git Bash, hooks are Python 3.14 scripts launched via the
pylauncher). Data from one unattended overnight run on 2026-09-03, with a few points not yet in this thread.Rate, with a denominator. The run made about 4,700 tool calls across 155 sessions (one lead session plus its subagents), with 7 to 9 hook processes spawned per call, so roughly 35,000 hook spawns. Seven of them froze. All seven had the signature above: one thread, zero CPU time, alive for hours. In our case the frozen process was the
py.exelauncher itself with nopython.exechild, i.e. the direct child the harness spawned, never the script.What the harness recorded. Nothing. For the lead session's wedged
Write(PostToolUse hook, configuredtimeout: 5), the transcript has notool_result, no "timed out - output discarded" line, and no API request for the next 6 h 21 min. Six user messages were dequeued into the transcript and never answered. The process stayed alive (background tasks fired, subagents kept running, TLS connections stayed open), so it looked like an API outage.Kill does un-wedge on this build. Unlike the reports above,
Stop-Process -Forceon the frozen hook resumed the lead session within 5 seconds, and killing the other twelve frozen hooks released every stalled subagent session (13 of 13). So the harness on 2.1.259 is waiting on the child and proceeds when it dies.Resume is still the better cure. A logon-scheduled watchdog polling every 10 s, criteria: candidate-named process (python/py/node/cmd/bash), age >= 15 s, kernel+user CPU == 0, then
ResumeThreadon each thread; kill only if a resumed process is still at zero CPU 20 s later. Live-fired againstCREATE_SUSPENDEDprocesses at suspend counts 1, 3, 20 and 100: resume path and kill path both confirmed. Cost measured under 1% of one core.Happy to provide the transcript excerpts (tool_use with no tool_result, queue-operation records) and the process-table capture if useful.