From 6638af2e22cbcd55ccae302dafc0936d821d9ad2 Mon Sep 17 00:00:00 2001 From: Felix Isaac Lim <38658663+FelixIsaac@users.noreply.github.com> Date: Sat, 6 Jun 2026 21:26:13 +0800 Subject: [PATCH] fix: prevent process hang on Windows when stdin EOF does not propagate through Volta shim On Windows with a Volta-managed ccstatusline, spawning ccstatusline as a child subprocess causes it to hang indefinitely. The process chain is: parent node -> cmd.exe (shell:true) -> bash (Volta shim) -> volta -> node The for-await stdin loop never receives EOF because stdin EOF from the parent does not propagate through cmd.exe -> bash -> volta on Windows. Each Claude Code statusline refresh spawns a new hung process; in one real session this accumulated to 961 leaked node.exe processes, 2.1 GB RAM, 90.5% kernel-mode CPU. Fix 1 (readStdin): replace for-await with event-based reading + 3s bail timeout. When EOF never arrives, the timeout destroys stdin and resolves the promise. Data already accumulated in chunks[] is returned normally (no data loss). Fix 2 (main): add explicit process.exit(0) after renderMultipleLines. Claude Code abandons (does not kill) statusline processes that run long; an explicit exit prevents any residual async handles from keeping the event loop alive. Related: nodejs/node#32291, volta-cli/volta#1199, ryoppippi/ccusage#459 --- src/ccstatusline.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/ccstatusline.ts b/src/ccstatusline.ts index a6890a5d..a14dd717 100644 --- a/src/ccstatusline.ts +++ b/src/ccstatusline.ts @@ -65,11 +65,21 @@ async function readStdin(): Promise { chunks.push(decoder.decode(chunk)); } } else { - // Node.js environment + // Node.js environment. + // Use event-based reading instead of for-await so we can add a bail + // timeout: on Windows, stdin EOF never propagates through the Volta shim + // chain (cmd.exe -> bash -> volta -> node), causing for-await to hang + // indefinitely. The timeout destroys stdin after 3s of silence; data + // already in chunks[] is preserved and returned normally. + // See: nodejs/node#32291, volta-cli/volta#1199, ryoppippi/ccusage#459 process.stdin.setEncoding('utf8'); - for await (const chunk of process.stdin) { - chunks.push(chunk as string); - } + await new Promise((resolve) => { + const bail = setTimeout(() => { process.stdin.destroy(); resolve(); }, 3000); + if (typeof (bail as NodeJS.Timeout).unref === 'function') (bail as NodeJS.Timeout).unref(); + process.stdin.on('data', (chunk: string) => chunks.push(chunk)); + process.stdin.on('end', () => { clearTimeout(bail); resolve(); }); + process.stdin.on('error', () => { clearTimeout(bail); resolve(); }); + }); } return chunks.join(''); } catch { @@ -292,6 +302,10 @@ async function main() { } await renderMultipleLines(result.data); + // Explicitly exit so the event loop does not linger. Without this, + // any residual async handles from rendering keep the process alive, + // which accumulates under Claude Code's "abandon not kill" lifecycle. + process.exit(0); } catch (error) { console.error('Error parsing JSON:', error); process.exit(1);