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

Open 💬 1 comment Opened Jul 13, 2026 by MokebeGluszak

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 ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗