anthropics / anthropics/claude-code
Session hangs mid-turn after a PreToolUse hook returns: tool never dispatches (2.1.220, Windows)
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
### Preflight Checklist
- [x] I have searched [existing issues](https://github.com/anthropics/claude-code/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) 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?
# Session hangs mid-turn after a PreToolUse hook returns: tool never dispatches, no timeout, no error
## Environment
- Claude Code **2.1.220** (native binary, VS Code extension `anthropic.claude-code-2.1.220-win32-x64`)
- Windows 11 Pro 10.0.26200, PowerShell 7.6.6, Node v24.16.0
- Hooks configured in `~/.claude/settings.json`: one `UserPromptSubmit`, two `PreToolUse`
(matchers `Bash` and `PowerShell`, each with `if: "(git *)"`), all `pwsh` scripts
## Summary
A session stops mid-turn with no error, no timeout and no crash. `claude.exe` stays alive and its
event loop keeps ticking (background timers still fire), but the current turn never advances. The
last transcript entry is a `tool_use` with no matching `tool_result` — the tool is never dispatched.
Esc does nothing; a subsequent prompt is accepted into the queue and never processed. The only
recovery is killing the process and `--resume`.
It happens specifically on turns where a **PreToolUse hook ran and returned**. The hook is not the
blocker — see the evidence below, which is the point of this report.
## The fingerprint
1. Transcript's last line is `tool_use`, with no `tool_result` after it.
2. The hook process for that tool call has **already exited** (present in `Win32_Process` as a zombie,
absent from `Get-Process`).
3. `claude.exe` burns ~0.4–1.2% of one core — idle, not spinning.
4. The status message from the hook's `statusMessage` field stays on screen indefinitely, because the
parent never moves past that stage.
## Why this is not the hook (the part I can now prove)
After two earlier hangs on 2026-09-16 that *were* my own hook's fault (a script blocking on
`[Console]::In.ReadToEnd()` waiting for an EOF that never came), I rewrote the shared stdin reader to
return as soon as the received bytes parse as JSON, and added a trace log around it.
On the most recent occurrence the trace shows the full hook lifecycle completing in **106 ms**:
```
13:09:53.956 pid=43976 + 79ms review-before-commit start
13:09:53.983 pid=43976 + 103ms review-before-commit stdin ok - 548 byte(s) in 1 read(s), 16ms, event=PreToolUse
13:09:53.986 pid=43976 + 106ms review-before-commit verdict allow (silent) - not a commit
```
The transcript for that session goes silent at **13:09:53** — the same second. So: the hook started,
read its payload, emitted its verdict, and exited, and the session hung anyway. The hook's configured
timeout is 20 s and it finished in 106 ms, so no timeout was involved.
Note the verdict on that path is an **exit 0 with zero bytes on stdout** (the gate only acts on
commits; this command was not one). ~91% of this hook's invocations take that silent path. If the
parent distinguishes "hook produced no output" from "hook has not produced output yet" by waiting for
the stdout pipe to close, a zero-byte response is the case most likely to expose a race there. That
is speculation about the cause; everything above it is measured.
## Occurrences (after my own hook bug was fixed)
| When | Session uptime | Transcript | Hook that ran | Hook verdict reached? |
|---|---|---|---|---|
| 2026-09-17 ~10:06 | 26 h | 8.4 MB | `context-gauge` (UserPromptSubmit) | yes — cleared by probe in 1.2 s |
| 2026-09-18 12:24:20 | 4.4 h | 4.4 MB | `review-before-commit` (PreToolUse) | yes — process had exited |
| 2026-09-18 13:09:53 | ~27 min | 4.4 MB | `review-before-commit` (PreToolUse) | yes — trace log, 106 ms |
Session age and context size are **not** preconditions: the third occurrence was a 27-minute-old
session resumed from `--resume`. The 2026-09-17 instance additionally logged
`[event-loop-stall] Total stalls: 77, cumulative: 366957ms`.
## Ruled out
- **Hook blocking on stdin** — the original 09-16 cause; fixed, and disproven since by both a real
unclosed-pipe probe and the trace log above.
- **Nested shell wrapper** — `settings.json` previously had `"shell": "powershell"` *and* `pwsh -File`
in the command, so hooks ran nested and the timeout killed the wrapper rather than the script,
leaving an orphan holding the stdout pipe. Commands are now `& 'path\to\script.ps1'`.
- **Hook timeout** — 106 ms against a 20 s budget.
- **Orphaned tool children** — reaped; the hangs persist without them.
- **Windows event log / crash** — empty; the process never dies.
## Workaround
Kill `claude.exe` and `claude --resume `. Nothing is lost, as file edits are already on
disk, but the in-flight turn and its reasoning are gone.
## What would help
- A dispatch-stage timeout, so a lost hook verdict surfaces as an error rather than an indefinite hang.
- Debug logging at the PreToolUse boundary that records the verdict as received by the parent, so the
gap between "hook wrote it" and "parent consumed it" is observable without instrumenting the hooks.
- Confirmation of whether a zero-byte `exit 0` from a PreToolUse hook is handled on a different path
from one that writes a `permissionDecision` JSON object.
### What Should Happen?
The session should not hang.
### Error Messages/Logs
```shell
```
### Steps to Reproduce
This is hard to reproduce. Most of the time it doesn't happen. See description.
### Claude Model
Opus
### Is this a regression?
I don't know
### Last Working Version
_No response_
### Claude Code Version
2.1.220
### Platform
Anthropic API
### Operating System
Windows
### Terminal/Shell
VS Code integrated terminal
### Additional Information
_No response_
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by reproducing the Windows session hang with a PreToolUse hook that exits 0 with no stdout, then compare the hook process lifecycle with the transcript's final tool_use entry. Check whether the parent receives the hook verdict; done means the tool dispatches normally or a dispatch-stage timeout reports a clear error instead of hanging indefinitely.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- powershell
- Domain
- cli, devtools
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100