[Windows] Hook timeout kills only the Git Bash launcher; an orphaned grandchild keeps the stdout pipe open and the session hangs mid-turn indefinitely

Status Open
Reported on v2.1.267
Maintainer reply None cached
Activity 1 comment · opened Sep 23, 2026

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

View original on GitHub ↗

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