You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Processes pile up when Claude Code keeps stdin open #485
Hey! I ran into something weird — after a while my machine started crawling and I found 35+ ccstatusline processes all sitting there doing nothing.
What's happening
Claude Code spawns a new ccstatusline process each refresh cycle but keeps the stdin pipe open without sending EOF. Since readStdin() waits for EOF before processing (for await (const chunk of process.stdin)), each process just hangs indefinitely. They pile up fast with a short refreshInterval.
You can reproduce it like this:
# keeps pipe open, like Claude Code does
(echo '<status-json>'; sleep 60) | ccstatusline
# hangs forever
Workaround I found
Wrapping with head -1 forces EOF after the first line and things work fine:
Adding a read timeout in readStdin(), or resolving as soon as stdin goes idle, would probably handle this cleanly without needing the wrapper.
Not sure if this is also a Claude Code bug (it probably should close stdin), but figured it's worth flagging here too since ccstatusline could defend against it.
This looks like the same underlying bug as #420: readStdin()'s for await never returns when stdin EOF is delayed. @mrns reproduced it without Volta in #420 via sleep 30 | ccstatusline, so it is not Windows/Volta-specific. There is an open fix in #421 that adds a read timeout plus an explicit exit. It bounds the Node-side pileup, though it does not yet cover the Bun read path and has a couple of edge cases I left notes on in the PR. Cross-linking so the three threads stay connected.
oldest 143.7 hours (~6 days), newest 35.2 hours — so they accumulate steadily rather than in one burst
every one had a dead parent. The Claude Code sessions that spawned them had long since exited, and the processes were still sitting there
The dead-parent detail seems worth adding because it rules out one possible reading: these are not waiting on a live parent that might eventually close the pipe. The parent is gone, the pipe can never EOF, and readStdin()'s for await (const chunk of process.stdin) still never returns. Nothing reaps them.
Two things made this hard to diagnose on Windows, which may be useful for anyone else landing here:
1. Get-CimInstance Win32_Process reports processes that have already exited. 33 of the 60 could not be killed by Stop-Processortaskkill — both returned "There is no running instance of the task" — while WMI kept listing them with a full WorkingSetSize. Anyone measuring the leak through WMI will overstate both the count and the memory. Get-Process gives the real numbers.
2. Spawn rate compounds this quickly. With refreshInterval: 10 and 7 sessions that is ~42 spawns/minute. A small hang rate reaches 60 stragglers in a few days, which matches the age spread above almost exactly.
Reading the installed 2.2.22 bundle, two things look relevant to the fix in #421:
.kill( does not appear anywhere in dist/ccstatusline.js. Even with a read timeout added, there is no path that terminates a child that has already hung.
Of the sync-exec call sites (execSync / execFileSync / spawnSync), none passes a timeout option. Several shell out to git, gh, glab and ssh. Without timeout, Node blocks indefinitely, so a slow or unreachable remote is a second, independent way to hang — separate from the stdin path this issue describes. On this machine the repo lives on a Google Drive mount, where git is routinely slow.
The head -1 workaround in the original report does stop the accumulation. Raising refreshInterval only reduces the rate.
Not urgent from my side — posting because the dead-parent and WMI-phantom details did not appear in #420 or #485 and cost real time to work out.
Correcting two claims in my comment above — I re-read the bundle properly and both were wrong. Apologies for the noise.
1. "none passes a timeout option" — wrong. Six of the twelve synchronous exec sites in 2.2.22 do pass timeout: CLI_TIMEOUT (5000 ms), including all the network ones: gh, glab, ssh, and the --version / auth status probes. My earlier grep matched only the first ~160 characters after each call, which truncated before the options object on the longer lines.
The accurate version is narrower: one call site is missing it — the cached git command runner — while its sibling git call has it. I have filed that separately as #557 rather than leave it buried here, since it is an exec-path problem and this issue is about the stdin read.
2. ".kill( does not appear anywhere" — wrong. It appears once, as process.kill(process.pid, sig) inside the vendored signal-exit. That re-raises a signal on the current process; it is not a path that terminates a spawned child, so the underlying observation still holds, but the flat statement was incorrect.
The measurements in my original comment are unaffected: 60 orphans, 4,511 MB, oldest 143.7 hours, every one with a dead parent, and 33 of 60 unkillable WMI phantoms. Those were taken from the live process table rather than from the source.
Activity
CorticalCode commentedon Jun 27, 2026
This looks like the same underlying bug as #420:
readStdin()'sfor awaitnever returns when stdin EOF is delayed. @mrns reproduced it without Volta in #420 viasleep 30 | ccstatusline, so it is not Windows/Volta-specific. There is an open fix in #421 that adds a read timeout plus an explicit exit. It bounds the Node-side pileup, though it does not yet cover the Bun read path and has a couple of edge cases I left notes on in the PR. Cross-linking so the three threads stay connected.herman925 commentedon Aug 22, 2026
Another data point for this, from a Windows machine running 7 concurrent Claude Code sessions on ccstatusline 2.2.22.
Same symptom, larger and older than the 35 in the original report:
ccstatuslinenode processes, holding 4,511 MB residentThe dead-parent detail seems worth adding because it rules out one possible reading: these are not waiting on a live parent that might eventually close the pipe. The parent is gone, the pipe can never EOF, and
readStdin()'sfor await (const chunk of process.stdin)still never returns. Nothing reaps them.Two things made this hard to diagnose on Windows, which may be useful for anyone else landing here:
1.
Get-CimInstance Win32_Processreports processes that have already exited. 33 of the 60 could not be killed byStop-Processortaskkill— both returned "There is no running instance of the task" — while WMI kept listing them with a fullWorkingSetSize. Anyone measuring the leak through WMI will overstate both the count and the memory.Get-Processgives the real numbers.2. Spawn rate compounds this quickly. With
refreshInterval: 10and 7 sessions that is ~42 spawns/minute. A small hang rate reaches 60 stragglers in a few days, which matches the age spread above almost exactly.Reading the installed 2.2.22 bundle, two things look relevant to the fix in #421:
.kill(does not appear anywhere indist/ccstatusline.js. Even with a read timeout added, there is no path that terminates a child that has already hung.execSync/execFileSync/spawnSync), none passes atimeoutoption. Several shell out togit,gh,glabandssh. Withouttimeout, Node blocks indefinitely, so a slow or unreachable remote is a second, independent way to hang — separate from the stdin path this issue describes. On this machine the repo lives on a Google Drive mount, wheregitis routinely slow.The
head -1workaround in the original report does stop the accumulation. RaisingrefreshIntervalonly reduces the rate.Not urgent from my side — posting because the dead-parent and WMI-phantom details did not appear in #420 or #485 and cost real time to work out.
herman925 commentedon Aug 22, 2026
Correcting two claims in my comment above — I re-read the bundle properly and both were wrong. Apologies for the noise.
1. "none passes a
timeoutoption" — wrong. Six of the twelve synchronous exec sites in 2.2.22 do passtimeout: CLI_TIMEOUT(5000 ms), including all the network ones:gh,glab,ssh, and the--version/auth statusprobes. My earlier grep matched only the first ~160 characters after each call, which truncated before the options object on the longer lines.The accurate version is narrower: one call site is missing it — the cached git command runner — while its sibling
gitcall has it. I have filed that separately as #557 rather than leave it buried here, since it is an exec-path problem and this issue is about the stdin read.2. "
.kill(does not appear anywhere" — wrong. It appears once, asprocess.kill(process.pid, sig)inside the vendoredsignal-exit. That re-raises a signal on the current process; it is not a path that terminates a spawned child, so the underlying observation still holds, but the flat statement was incorrect.The measurements in my original comment are unaffected: 60 orphans, 4,511 MB, oldest 143.7 hours, every one with a dead parent, and 33 of 60 unkillable WMI phantoms. Those were taken from the live process table rather than from the source.