From 7f0950230ca7b216b1adff77d8dd7674597571cf Mon Sep 17 00:00:00 2001 From: Cole Leavitt Date: Sat, 21 Feb 2026 16:29:22 -0700 Subject: [PATCH] fix: kill process group on timeout and handle stdin EPIPE - Use detached process group (non-Windows) + process.kill(-pid) to kill the entire process tree, not just the outer shell wrapper - Add proc.stdin error listener to absorb EPIPE when child exits before stdin write completes --- .../command-executor/execute-hook-command.ts | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/shared/command-executor/execute-hook-command.ts b/src/shared/command-executor/execute-hook-command.ts index f6534e36..8dae500d 100644 --- a/src/shared/command-executor/execute-hook-command.ts +++ b/src/shared/command-executor/execute-hook-command.ts @@ -52,9 +52,11 @@ export async function executeHookCommand( let settled = false; let killTimer: ReturnType | null = null; + const isWin32 = process.platform === "win32"; const proc = spawn(finalCommand, { cwd, shell: true, + detached: !isWin32, env: { ...process.env, HOME: home, CLAUDE_PROJECT_DIR: cwd }, }); @@ -69,6 +71,7 @@ export async function executeHookCommand( stderr += data.toString(); }); + proc.stdin?.on("error", () => {}); proc.stdin?.write(stdin); proc.stdin?.end(); @@ -92,17 +95,23 @@ export async function executeHookCommand( settle({ exitCode: 1, stderr: err.message }); }); + const killProcessGroup = (signal: NodeJS.Signals) => { + try { + if (!isWin32 && proc.pid) { + process.kill(-proc.pid, signal); + } else { + proc.kill(signal); + } + } catch {} + }; + const timeoutTimer = setTimeout(() => { if (settled) return; - // Try graceful shutdown first - try { - proc.kill("SIGTERM"); - } catch {} + // Kill entire process group to avoid orphaned children + killProcessGroup("SIGTERM"); killTimer = setTimeout(() => { if (settled) return; - try { - proc.kill("SIGKILL"); - } catch {} + killProcessGroup("SIGKILL"); }, SIGKILL_GRACE_MS); // Append timeout notice to stderr stderr += `\nHook command timed out after ${timeoutMs}ms`;