Windows: hook processes are left SUSPENDED (never executed a single instruction), hanging the turn for 30-60+ minutes

Status Open
Maintainer reply None cached
Activity 6 comments · opened Jul 13, 2026

Environment

  • Claude Code on Windows 11 Pro (build 26200), Git Bash, PowerShell 5.1
  • Hooks: Python scripts (python.exe 3.12), declared with "timeout": 2 / "timeout": 3 in settings.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"

  1. WaitReason = Suspended cannot 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.

  1. Zero CPU time really means zero instructions. Control experiment: 12 python.exe processes

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".

  1. 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.

  1. 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.

  1. 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 under
UV_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).
  • Escape does 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

  1. On timeout, actually terminate the hook process. Today "output discarded" leaves it resident

forever. (See #67888 — the cleanup path itself is unreliable on Windows.)

  1. Resume before waiting / verify the child is runnable after spawn — a suspended child will

never produce output, so waiting on it is unbounded.

  1. Make Escape cancel 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.

View original on GitHub ↗

3 Comments

MokebeGluszak · 3 months ago

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 single ResumeThread() 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 Escape unable 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 / UserProcessorTime dump, ResumeThread probe) from the original report.

Onward-Mundy · 2 months ago

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

  1. Not Python-specific. One suspended child was Node.js (a plugin MCP broker script), left 13.5 h. Whatever suspends the process is upstream of the runtime.
  2. Claude Code's own inline if-condition evaluator child gets suspended too (the python3 -c "…os.environ['PATTERNS']…" process that evaluates hook if: gates). That reproduces the defect with no plugin code in the child at all.
  3. if-gated hooks that never fire still spawn the evaluator on every matching tool event — each spawn is a fresh exposure to the race.
  4. The wedged command provably never starts. While a session showed a 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).
  5. Killing the suspended child does not un-wedge the session — the pending tool call still never resolves (tested twice). Only exiting and restarting the client worked (claude -c afterward was fine — it's the process that matters), consistent with your report.
  6. Suspended orphans survive client restarts. Three of them outlived the session that spawned them by hours; nothing reaps them until manual kill or reboot.

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)

  • Hook script bugs — the suspended children never executed an instruction; the same scripts pass standalone harnesses (bounded stdin reads, parent-killed-mid-read chains) with clean exits
  • The WindowsApps python3 Store shim as the cause — suspended children include a real CPython 3.12 install and node
  • AV/ASR interference — blocks fail fast with Access is denied; they never hang (verified by live probe)
  • git locks / auth / network — verified absent during a live wedge
  • Permission-mode/classifier delays — stall-rate analysis over 6,600 tool calls across 120 session transcripts showed no correlation

Happy to provide the full capture log (process tables, timeline, transcript excerpts with tool_use/tool_result pairing) if useful.

CSLewin · 1 month ago

Still present on 2.1.259 (Windows 11 Pro build 26200, Git Bash, hooks are Python 3.14 scripts launched via the py launcher). 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.exe launcher itself with no python.exe child, 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, configured timeout: 5), the transcript has no tool_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 -Force on 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 ResumeThread on each thread; kill only if a resumed process is still at zero CPU 20 s later. Live-fired against CREATE_SUSPENDED processes 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.

Showing cached comments. Read the full discussion on GitHub ↗