Skip to content

[Windows] Process hangs indefinitely when spawned as child subprocess via Volta shim (stdin EOF not propagated through cmd.exe → bash → volta → node chain) #420

Open

Description

@FelixIsaac

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();

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.

Activity

  1. mrns commented on Jun 25, 2026

    @mrns

    Reproduces without Volta — the root cause is more general than the Volta shim.

    Setup: Windows 11, ccstatusline 2.2.22, Node v24.14.0, Claude Code with statusLine.command: "ccstatusline". No Volta anywhere — Claude Code invokes the command through its bundled Git Bash (sh.exe → node). Same accumulation described here: over a long session it built to 280+ stuck node/sh processes (~11 GB RAM, CPU pegged at 100%).

    Minimal, shim-independent repro (no Volta, no Claude Code):

    echo '{}' | ccstatusline   # exits in <1s
    sleep 30 | ccstatusline    # hangs the full 30s — blocks until stdin EOF
    

    The trigger is simply stdin never reaching EOF promptly; any Windows process chain that delays EOF (Volta, Git Bash/MSYS, …) hits it.
    So "configuring it directly as the command works correctly" isn't universal — it depends on how the host wires up stdin on Windows.

    This supports #421's approach (stdin read timeout + explicit exit). +1 for merging.

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