[Windows] Hook timeout kills only the Git Bash launcher; an orphaned grandchild keeps the stdout pipe open and the session hangs mid-turn indefinitely
Preflight Checklist
- [x] I have searched existing issues 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?
On Windows, when a command hook exceeds its timeout, Claude Code terminates only the process it spawned: the Git for Windows launcher Git\bin\bash.exe. It does not end the launcher's children.
Under load (several hooks running in parallel), the launcher's child, Git\usr\bin\bash.exe -c "<hook command>" or the hook script beneath it, can be left running with a dead parent, 0% CPU, no children and a single thread. It never exits.
That orphan still holds the inherited stdout pipe, so the pipe never reaches EOF. The tool call's tool_result is never delivered, and the session sits "busy" mid-turn with no timeout of its own.
Killing that one orphaned process releases the session within seconds, and the turn continues normally.
Live case: an interactive session stayed wedged for 17 minutes after an Edit. Its transcript showed the Edit tool_use with no tool_result. An orphaned usr\bin\bash.exe <hook>.sh (dead parent, 0 CPU) had started 8 seconds after that tool call. Stopping that process let the session resume within 15 seconds.
What Should Happen?
When a hook times out, its whole process tree is ended (or Claude Code stops waiting for EOF on the pipe once the process it spawned is gone), a hook timeout is reported, and the session continues.
Error Messages/Logs
No error is shown; the session simply never proceeds.
- Transcript: the last entry is an `Edit` `tool_use` with no matching `tool_result`, followed by `hook_cancelled` / `hook_success` attachments for the `PostToolUse:Edit` hooks.
- Hook timeouts are rising. Transcript attachments of type `hook_cancelled` with `"timedOut": true` went from 0-11 a day (versions 2.1.267-2.1.274) to 40-69 a day (2.1.275-2.1.280). Each one risks this hang.
- A bounded stdin read inside the hook (`read -t 2`) does not help. It returns correctly on a held-open pipe, and the frozen process can be the `-c` wrapper, stuck before any hook code runs.
Steps to Reproduce
Minimal repro without Claude Code. It simulates what the hook timeout does, which is terminate the launcher only.
hook.sh:
#!/bin/bash
# stand-in for a hook: a few forks and ~2s of work
for i in 1 2 3; do x=$(date); sleep 0.5; done
echo done
storm.py:
import os, random, subprocess, threading, time
LAUNCHER = os.path.expandvars(r"%LOCALAPPDATA%\Programs\Git\bin\bash.exe") # adjust to your Git install
N, TAG, procs = 24, f"STORM{int(time.time())}", []
def run(i):
p = subprocess.Popen([LAUNCHER, "-c", f"bash hook.sh {TAG}_{i}"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
p.stdin.write(b"{}"); p.stdin.close()
time.sleep(random.uniform(0.05, 3.0))
p.kill() # what the hook timeout does: terminate the launcher only
procs.append(p)
ts = [threading.Thread(target=run, args=(i,)) for i in range(N)]
[t.start() for t in ts]; [t.join() for t in ts]
time.sleep(25)
stuck = 0
for p in procs: # a pipe that never reaches EOF = a hung tool call
t = threading.Thread(target=p.stdout.read, daemon=True); t.start(); t.join(0.5)
stuck += t.is_alive()
print(f"{stuck} of {N} stdout pipes never reached EOF")
Result: 3 of 24 pipes never reached EOF. The surviving processes were usr\bin\bash.exe with a dead parent and 0 CPU. Killing the launcher once, on an idle machine, did not reproduce it; it needs concurrency.
In Claude Code itself: configure several PostToolUse command hooks (bash scripts, timeout: 5) on Edit|Write and make many edits while the machine is busy.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.280
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Windows Terminal
Additional Information
Suggested fix: on Windows, run each hook in a job object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE and close the job on timeout, or terminate the full process tree. Either ends every descendant holding the pipe. Stopping the wait for EOF once the direct child is dead would also prevent the hang.
Workaround we are using: an external watcher that finds bash.exe processes running a hook script whose parent process is dead and whose age exceeds the hook's timeout, and force-kills them.
Environment: Windows 11 Enterprise (build 26200), Git for Windows, CLAUDE_CODE_GIT_BASH_PATH set to Git\bin\bash.exe.
Related but different: #95354 (Windows hang after a PreToolUse hook that finished normally, with no process left behind), #90585 (Linux hook blocked on stdin, closed), #92583 (Windows Bash tool orphans after the session ends).
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗