Summary
When ccstatusline is invoked as a child process from another Node.js script using spawn('ccstatusline', [], { shell: true }) on Windows with a Volta-managed installation, the process hangs indefinitely and never exits. Each invocation leaks one node.exe process. In a long Claude Code session this accumulated to 961 leaked processes → 2.1 GB RAM → 90.5% kernel-mode CPU from Windows scheduler overhead.
Root cause
The Volta-installed ccstatusline binary is a bash shim:
volta run "$(basename $0)" "$@"
When spawned via spawn('ccstatusline', [], { shell: true }), the process chain on Windows is:
parent node → cmd.exe (shell:true) → bash (Volta shim) → volta → node (ccstatusline)
ccstatusline reads stdin with for await (const chunk of process.stdin) (line ~77342 in dist). The parent calls cc.stdin.end() to signal EOF — but on Windows, stdin EOF does not propagate through the cmd.exe → bash → volta chain. The inner for await loop never terminates. ccstatusline sits waiting forever. The parent also hangs waiting on cc.on('close', ...).
Claude Code's statusline lifecycle abandons (does not kill) a hung process when a new refresh triggers, so processes accumulate indefinitely.
This is a known Node.js/Windows issue
The fix (2 lines in src/ccstatusline.ts)
After renderMultipleLines writes its output, instead of relying on event loop drain (which doesn't work through the Volta chain):
// After rendering is complete:
process.stdout.write(formattedOutput, () => {
process.stdin.destroy(); // unblock event loop; EOF won't arrive through Volta chain on Windows
process.exit(0); // guarantee exit
});
Or more simply, wherever main() finishes rendering and before it returns:
process.stdin.destroy() is sufficient — it unregisters the stdin handle from the event loop so Node exits naturally without waiting for EOF that will never arrive.
Notes
- This does not affect the primary use case (
"command": "ccstatusline" directly in Claude Code settings) — in that case Claude Code pipes directly to ccstatusline with a single process layer and EOF propagates correctly.
- The bug only manifests when ccstatusline is used as a pipe-through subprocess, which is a misuse pattern but one that
statusline-wrapper.sh documentation may inadvertently encourage.
- ccstatusline's Custom Command Widget already has a 1000ms kill timeout which correctly prevents this class of bug for child widgets — the same pattern (guaranteed exit) should be applied to the main process.
Environment
- Windows 11 Pro 10.0.26200
- ccstatusline v2.2.19
- Volta-managed Node installation
- Node.js v24.15.0
Suggested additional fix (documentation)
Add a note to docs/WINDOWS.md warning that spawning ccstatusline as a child_process subprocess with shell: true on Windows will hang due to Volta's bash shim. The correct usage is as the direct statusLine command.
Summary
When
ccstatuslineis invoked as a child process from another Node.js script usingspawn('ccstatusline', [], { shell: true })on Windows with a Volta-managed installation, the process hangs indefinitely and never exits. Each invocation leaks onenode.exeprocess. In a long Claude Code session this accumulated to 961 leaked processes → 2.1 GB RAM → 90.5% kernel-mode CPU from Windows scheduler overhead.Root cause
The Volta-installed
ccstatuslinebinary is a bash shim:When spawned via
spawn('ccstatusline', [], { shell: true }), the process chain on Windows is:ccstatuslinereads stdin withfor await (const chunk of process.stdin)(line ~77342 in dist). The parent callscc.stdin.end()to signal EOF — but on Windows, stdin EOF does not propagate through thecmd.exe → bash → voltachain. The innerfor awaitloop never terminates. ccstatusline sits waiting forever. The parent also hangs waiting oncc.on('close', ...).Claude Code's statusline lifecycle abandons (does not kill) a hung process when a new refresh triggers, so processes accumulate indefinitely.
This is a known Node.js/Windows issue
process.stdinnever returns with using Git Bash for Windows nodejs/node#5620 —process.stdinnever returns in Git Bash/Windowsprocess.stdintochild.stdinleaves behind an open handle nodejs/node#32291 — pipingprocess.stdintochild.stdinleaves open handlespawnwith stdio/stdin 'pipe' options causes pwsh shell to hang after execution nodejs/node#52364 —spawn+stdin:'pipe'causes pwsh to hang (still broken 2024)child_process.execincmd(Windows) volta-cli/volta#1199 — Volta shims + child_process confirmed broken on WindowsThe fix (2 lines in src/ccstatusline.ts)
After
renderMultipleLineswrites its output, instead of relying on event loop drain (which doesn't work through the Volta chain):Or more simply, wherever
main()finishes rendering and before it returns:process.stdin.destroy()is sufficient — it unregisters the stdin handle from the event loop so Node exits naturally without waiting for EOF that will never arrive.Notes
"command": "ccstatusline"directly in Claude Code settings) — in that case Claude Code pipes directly to ccstatusline with a single process layer and EOF propagates correctly.statusline-wrapper.shdocumentation may inadvertently encourage.Environment
Suggested additional fix (documentation)
Add a note to
docs/WINDOWS.mdwarning that spawning ccstatusline as achild_processsubprocess withshell: trueon Windows will hang due to Volta's bash shim. The correct usage is as the direct statusLine command.