Skip to content

cli hangs forever reading stdin when no argument-bearing token follows the tool name and stdin is an open pipe (default Node spawn deadlocks) #1359

Closed

Description

@artaommahe

Version

codebase-memory-mcp 0.9.1-rc.1

Platform

macOS (Apple Silicon)

Install channel

GitHub release archive / install.sh / install.ps1

Binary variant

standard

What happened, and what did you expect?

codebase-memory-mcp cli <tool> invoked with no flags blocks forever reading stdin whenever stdin is an open pipe that never delivers data. It produces no output and never exits.

This hits tools that legitimately need no arguments — list_projects is the obvious one. Passing any flag avoids it (index_status --project X and query_graph --project X --query "..." both complete normally against the same open-pipe stdin), so the trigger is specifically "no flags given → unconditionally read args from stdin".

That is a deadlock for automation. Any hook, CI step, git hook, or agent harness that runs cli list_projects while stdin is an inherited pipe hangs indefinitely rather than failing. I hit it by accident: a shell script doing

P=$(codebase-memory-mcp cli list_projects | grep -o '...' | head -1)

hung, and the process was still stuck 14 minutes later. A sample(1) of the stuck process shows the main thread parked in fread on fd 0:

2602 Thread_13001004   DispatchQueue_1: com.apple.main-thread  (serial)
  + 2602 start  (in dyld) + 6992
  +   2602 ??? (in codebase-memory-mcp) + 0x5474
  +     2602 ??? (in codebase-memory-mcp) + 0x5f9c
  +       2602 ??? (in codebase-memory-mcp) + 0x8768
  +         2602 fread  (in libsystem_c.dylib) + 136
  +           2602 __fread  (in libsystem_c.dylib) + 488
  +             2602 __srefill1  (in libsystem_c.dylib) + 36
  +               2602 _sread  (in libsystem_c.dylib) + 32
  +                 2602 __sread  (in libsystem_c.dylib) + 24
  +                   2602 __read_nocancel  (in libsystem_kernel.dylib) + 8

Expected: only read stdin when it is actually ready/has data (select/poll with a zero timeout, or check isatty/S_ISFIFO plus a non-blocking peek), or only when the tool actually requires arguments that weren't supplied. A tool that needs no arguments should never block on stdin. Failing that, a short read timeout with a clear error beats an unbounded hang.

I assume this arrives with the RC's new "args via piped stdin" input path (echo '<json>' | codebase-memory-mcp cli <tool>, per the deprecation notice on raw-JSON args).

Reproduction

No repo needed — any indexed project, or none at all.

# stdin is an open pipe with a writer that never writes
mkfifo /tmp/cbm-fifo
sleep 120 > /tmp/cbm-fifo &

# hangs forever, zero bytes of output:
codebase-memory-mcp cli list_projects < /tmp/cbm-fifo

# control — returns immediately, 896 bytes:
codebase-memory-mcp cli list_projects < /dev/null

# control — flags present, same open-pipe stdin, completes fine:
codebase-memory-mcp cli index_status --project <some-project> < /tmp/cbm-fifo

Measured with a 12-second watchdog (perl -e 'alarm 12; exec @ARGV'):

invocation stdin result
cli list_projects FIFO, held open, no data hang, 0 bytes, killed by watchdog
cli list_projects /dev/null exit 0, 896 bytes
cli index_status --project cbm-rc-repros FIFO, held open, no data exit 0, 31,970 bytes
cli query_graph --project cbm-rc-repros --query "MATCH (n) RETURN n.name LIMIT 1" FIFO, held open, no data exit 0, 50 bytes

Confirmations

  • I searched existing issues and this is not a duplicate.
  • My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.

Posted by Claude Code on behalf of @artaommahe

Activity

  1. added
    parsing/qualityGraph extraction bugs, false positives, missing edges
    stability/performanceServer crashes, OOM, hangs, high CPU/memory
    windowsWindows-specific issues
    on Jul 30, 2026
  2. artaommahe commented on Jul 30, 2026

    @artaommahe
    Author

    My stated trigger is wrong and would send a fix in the wrong direction. Correcting it, and adding a realistic repro that replaces the contrived one.

    Correction: "passing any flag avoids it" is false

    --json and --progress are flags, and both still hang. Re-measured, FIFO stdin held open by a writer that never writes, 8-second watchdog:

    invocation stdin result
    cli list_projects FIFO hang, 0 bytes
    cli list_projects --json FIFO hang, 0 bytes
    cli list_projects --progress FIFO hang, 0 bytes
    cli index_status (required --project omitted) FIFO hang, 0 bytes
    cli list_projects '{}' (positional) FIFO exit 0, 643 bytes
    cli get_graph_schema --project X FIFO exit 0, 2,925 bytes
    cli index_status --project X FIFO exit 0, 31,970 bytes
    cli list_projects /dev/null exit 0, 643 bytes

    So it isn't "no flags". Observationally the read is attempted when the first argument-bearing token after the tool name is absent — --json/--progress are consumed as CLI options before that check and don't count, and a positional {} or a --flag value pair does.

    Two consequences I under-sold in the original report:

    1. cli index_status with the required --project omitted hangs, where the same command with < /dev/null prints a perfectly good missing required argument: project. So the ordinary "I forgot a flag" path deadlocks instead of erroring — that's a much more common way to hit this than my FIFO scenario.
    2. list_projects's schema has zero properties ({"type":"object","properties":{}} in tools/list). The process performs an unbounded read for a tool that cannot accept an argument at all.

    Also: interactive use is unaffected — under a real pty, output is produced immediately. So this only bites non-interactive callers, which is exactly where it's hardest to notice.

    Replace my repro with this one — it isn't contrived

    mkfifo invites "who does that?". Node's child_process.spawn defaults to stdio: ['pipe','pipe','pipe'], and the parent must explicitly call child.stdin.end(). Almost nobody does that for a command they aren't writing to. Verified on this machine:

    const { spawn } = require('child_process');
    const BIN = '/path/to/codebase-memory-mcp';
    
    // default spawn — stdin is an open pipe nobody ever closes
    spawn(BIN, ['cli', 'list_projects'], {});
    //   -> TIMEOUT after 8s, 0 bytes of output
    
    // same call, stdin closed
    spawn(BIN, ['cli', 'list_projects'], { stdio: ['ignore', 'pipe', 'pipe'] });
    //   -> exit=0, 643 bytes

    That is the ordinary way a Node or Electron wrapper, VS Code extension, or MCP-adjacent tool shells out. My own shell script hit the same thing and sat stuck for 14 minutes before I noticed.

    Narrowing my title claim

    "hooks/CI" is too broad and I should correct it: GitHub Actions gives steps /dev/null, and a hook that writes JSON and closes gets EOF, so neither hangs. The accurate scope is any wrapper that spawns the binary with default piped stdin — Node/Electron especially.

    Revised ask

    My original "check isatty" suggestion is likely redundant given interactive behaviour already differs. The substantive requests are:

    1. Don't attempt an args read for a tool whose schema has no properties.
    2. Bound the read — poll/select with a short deadline, or read only when data is already available — so a never-delivering pipe produces an error rather than an indefinite block.
    3. When required arguments are missing and no args arrive, emit the existing missing required argument error rather than blocking.

    Possibly a duplicate of a symptom already reported

    #1195 ("list_projects hangs indefinitely with 2 indexed projects, v0.9.0, macOS arm64, cli") looks like this bug diagnosed as a project-count problem — same tool, same platform, same CLI surface. Worth checking whether that reporter's invocation had piped stdin; if so, this is the root cause and that issue can be closed against it.


    Posted by Claude Code on behalf of @artaommahe

  3. changed the title [-]cli hangs forever reading stdin when invoked with no flags and stdin is an open pipe (deadlocks hooks/CI/agent harnesses)[/-] [+]cli hangs forever reading stdin when no argument-bearing token follows the tool name and stdin is an open pipe (default Node spawn deadlocks)[/+] on Jul 30, 2026
  4. added
    bugSomething isn't working
    and removed
    parsing/qualityGraph extraction bugs, false positives, missing edges
    on Aug 3, 2026
  5. added this to the 0.9.1-rc milestone on Aug 3, 2026
  6. added
    ux/behaviorDisplay bugs, docs, adoption UX
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    and removed
    windowsWindows-specific issues
    on Aug 3, 2026
  7. DeusData commented on Aug 3, 2026

    @DeusData
    Owner

    Thank you for correcting the trigger and replacing the FIFO-only example with the default Node spawn case. Current CLI argument resolution treats any non-TTY stdin as piped JSON and fully slurps it before dispatch, so an open pipe with no data blocks indefinitely even for a zero-argument tool. This is distinct from #1195, whose hangs are state-dependent across cache enumeration and occur with completed stdin. I have removed the unrelated parsing/Windows labels and routed this at high priority in 0.9.1-rc.

  8. DeusData commented on Aug 14, 2026

    @DeusData
    Owner

    @artaommahe — thank you twice over: once for the sample(1) trace parked in fread on fd 0, and once for coming back to correct your own trigger. The corrected table (with --json and --progress still hanging, and index_status deadlocking when its required flag is merely forgotten) is what makes this concrete, and replacing the mkfifo repro with a default Node spawn was exactly right — that is how most callers will actually hit it.

    Confirmed in code: when stdin is not a TTY, src/main.c slurps it to EOF as the argument source, so a pipe that nobody ever closes blocks forever. This is being fixed and is in scope for tonight's v0.10.5, which is aimed at the issues that make cbm impossible to operate.

    Sitting stuck for 14 minutes because the tool was waiting on a pipe that was never going to speak is a bad way to spend an afternoon. Sorry.

  9. added a commit that references this issue on Aug 15, 2026
  10. added 2 commits that reference this issue on Aug 18, 2026
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

    bugSomething isn't workingeditor/integrationEditor compatibility and CLI integrationpriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.stability/performanceServer crashes, OOM, hangs, high CPU/memoryux/behaviorDisplay bugs, docs, adoption UX

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions