Skip to content

Processes pile up when Claude Code keeps stdin open #485

Open

Description

@shaharke

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:

"statusLine": {
  "command": "bash -c 'head -1 | ccstatusline'"
}

Possible fix

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.

Thanks for the great tool!

Activity

  1. CorticalCode commented on Jun 27, 2026

    @CorticalCode
    Contributor

    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.

  2. herman925 commented on Aug 22, 2026

    @herman925

    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:

    • 60 orphaned ccstatusline node processes, holding 4,511 MB resident
    • 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-Process or taskkill — 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.

  3. herman925 commented on Aug 22, 2026

    @herman925

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions