fix: prevent process hang on Windows when stdin EOF does not propagate through Volta shim#421
Open
FelixIsaac wants to merge 1 commit into
Open
fix: prevent process hang on Windows when stdin EOF does not propagate through Volta shim#421FelixIsaac wants to merge 1 commit into
FelixIsaac wants to merge 1 commit into
Conversation
…e 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On Windows with a Volta-managed ccstatusline, spawning ccstatusline as a child subprocess causes it to hang indefinitely. The process chain:
The
for await (const chunk of process.stdin)loop never terminates because stdin EOF from the parent does not propagate throughcmd.exe → bash → voltaon Windows. Claude Code's statusline lifecycle abandons (does not kill) processes that take too long — so each refresh leaks one hung node.exe process.Real-world impact: 961 leaked node.exe processes, 2.1 GB RAM, 90.5% kernel-mode CPU from Windows scheduler overhead managing ~1000 sleeping processes.
This is a known Windows/Node.js behavior:
process.stdintochild.stdinleaves behind an open handle nodejs/node#32291 — piping stdin to child.stdin leaves open handlespawnwith stdio/stdin 'pipe' options causes pwsh shell to hang after execution nodejs/node#52364 — spawn + stdin:pipe causes pwsh to hang (2024)child_process.execincmd(Windows) volta-cli/volta#1199 — Volta shims + child_process broken on Windows cmdNote: the normal use case (
"command": "ccstatusline"direct from Claude Code) is not affected — Claude Code pipes directly, single process layer, EOF works correctly.Fix
Fix 1 —
readStdin(): Replacefor awaitwith event-based reading + 3s bail timeout. When EOF never arrives (Volta chain), the timeout destroys stdin and resolves the Promise. Data already accumulated inchunks[]is preserved and returned normally — no data loss.Fix 2 —
main(): Add explicitprocess.exit(0)afterrenderMultipleLines. Claude Code's "abandon not kill" lifecycle means any residual async handles from rendering keep the process alive indefinitely. An explicit exit prevents accumulation.Build note
Bun was not available in this environment so
dist/was not rebuilt. The source change is insrc/ccstatusline.tsonly — maintainer build required before publishing.