Repository navigation
Command hooks always hit timeoutSec after the first JS-side child spawn (hook exits in ~20 ms but stays a zombie until timeout) #5154
Description
Describe the bug
In a long-running interactive session, every command hook starts hitting the full timeoutSec (5 s), even though the hook process exits in about 20 ms. Each tool call runs up to three hooks (preToolUse, permissionRequest, postToolUse), so every tool call can take up to 15 s longer. On one machine this added 4 to 8 hours of waiting per day across sessions.
From inside an affected process, it looks like the CLI stops noticing when hook child processes exit:
- The hook process exits immediately but isn't reaped. I sampled
ps --ppid <copilot pid>every 200 ms. Each hook'sbashchild went<defunct>(zombie) within milliseconds and stayed that way for 4.6 to 4.9 s, until the 5 s timeout fired:2484229 zombie_for=4.6s 2494373 zombie_for=4.9s 2497495 zombie_for=4.9s - The hook script itself finishes fast. I timestamped the hook script at start, at stdin EOF, and at the end. In the affected process, stdin reached EOF about 7 ms after start and the script finished about 13 ms later, yet the CLI still logged
HookTimeoutError: Hook command timed out after 5 secondsfor those calls. - Nothing else holds the pipe open. I scanned
/proc/*/fd0.5 s after each hook started for any other holder of the hook's stdout pipe. Across 22 timed-out hooks, none was found. - Fresh processes are fine. The same hook config runs normally (around 20 ms) in a new interactive session and in
copilot -p.
What seems to trigger it: in 6 of the 7 affected processes, the first timeout came shortly (13 s to 7 min) after the first clipboard write, which spawns a child process from the JS side:
07:53:38 [WARNING] writeToClipboard: clip.exe failed; relied on OSC 52: Error: clip.exe exited with code 1
07:54:45 [WARNING] [rust:hooks] Hook from ".../hooks/orca.json" timed out; allowing processing to proceed: HookTimeoutError: Hook command timed out after 5 seconds
The 7th was a resumed session that started an MCP server and a native extension at launch, then timed out on its first hook. After the first timeout, the process never recovered: one process logged 2,960 timeouts over about 6 hours. Processes without hooks configured were unaffected even after clipboard writes.
One possible cause, which I haven't verified: the JS runtime (libuv) installs its own SIGCHLD handler when it first spawns a child. If that handler replaces the Rust hook runner's handler instead of chaining to it, the runner would never be told its hook children have exited. It would then only find out when its timeout fires and it cleans up. That matches both the zombies lasting until the timeout and the state starting at the first JS-side spawn.
This started on October 13, when 1.0.96 was installed. From October 7 to 12 the same hook config ran about 2,000 to 4,000 hooks a day with zero timeouts. Since then, 16%, 40% and 61% of hooks per day have timed out (1.0.96 to 1.0.98).
Affected version
GitHub Copilot CLI 1.0.98 (also seen on 1.0.96 and 1.0.97)
Steps to reproduce the behavior
- Put any fast command hook in
~/.copilot/hooks/forPreToolUse,PostToolUseandPermissionRequest, for example a script that prints{}and reads stdin. My setup is the Orca agent hook with"timeoutSec": 5. - Start an interactive
copilotsession and run a few tool calls. The hooks finish in about 20 ms. - Do something that spawns a child process from the JS side, such as copying a response to the clipboard. On WSL this runs
clip.exe. - Run more tool calls. Every hook now takes the full
timeoutSecand logsHookTimeoutError, andps --ppid <copilot pid>shows the hook's shell as<defunct>for abouttimeoutSec.
Expected behavior
The CLI should reap a hook as soon as its process exits and continue right away. A hook that finishes in 20 ms shouldn't block a tool call for 5 s.
Additional context
- OS: Ubuntu on WSL2 (kernel 6.18.54.1-microsoft-standard-WSL2), x86_64
- Shell: bash. Installed via npm (
@github/copilot), Node v24 - Started with
--yolo - Workaround: lower
timeoutSecor remove the hook config.
Activity