[BUG] --output-format stream-json intermittently hangs — process stops producing output and never exits

Resolved 💬 5 comments Opened Mar 27, 2026 by RandaZraik Closed Apr 30, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When spawning claude -p --output-format stream-json --verbose as a subprocess with stdout redirected to a file (non-TTY), the CLI intermittently stops producing output and hangs indefinitely. The process never exits and must be force-killed. The {"type":"result"} event is never emitted.

Reproduced on v2.1.85 — 2 out of 12 runs hung when launching 6 concurrent instances with tool-use prompts.

In our reproduced cases, the hang occurred mid-conversation — the last event written was a tool_result ("type":"user"). The CLI delivered the tool result but the next assistant turn never arrived. The process produced zero further output and never exited.

Key observations:

  • Stdout redirected to a file (not a TTY), prompt passed via stdin
  • Intermittent — same prompt succeeds on some runs and hangs on others
  • Concurrency increases reproduction rate — 6 parallel instances reproduced in ~12 runs
  • No output on stderr — the process is silently stuck
  • The hang can occur mid-conversation (after tool_result, waiting for next assistant turn) or after the final assistant response (result event never written)

Previously reported in #1920, #21099, and #25629 — all auto-closed for inactivity, not fixed.

What Should Happen?

The CLI must always either:

  1. Complete the conversation, emit {"type":"result"}, and exit 0, or
  2. Emit an error and exit non-zero

It should never hang with no output and no exit.

Error Messages/Logs

No error on stderr. The process hangs silently.

**Successful run:**

system -> assistant (tool_use) -> rate_limit_event -> user (tool_result) -> assistant (text) -> result
[exits 0]


**Failing run (from our reproduction — run 10):**

system -> system -> system -> assistant (tool_use) -> rate_limit_event -> user (tool_result) -> assistant (tool_use) -> user (tool_result)
[hangs — no more output, no exit, no stderr]

Steps to Reproduce

Save as repro.py and run python repro.py:

"""Launches concurrent claude -p processes to reproduce the hang."""
import subprocess, os, time, tempfile, json, concurrent.futures

CONCURRENCY = 6
TIMEOUT = 120

PROMPTS = [
    "Create /tmp/repro_a.txt with 'hello', read it, delete it, confirm deletion.",
    "Run `echo test > /tmp/repro_b.txt && cat /tmp/repro_b.txt && rm /tmp/repro_b.txt` and report.",
    "Create /tmp/repro_c.py with a fibonacci function, run it, report the result.",
    "List files in /tmp, count them, create /tmp/repro_d.txt with that count.",
    "Write a bash one-liner to /tmp/repro_e.sh that prints the date, chmod +x, run it.",
    "Create /tmp/repro_f.json with {\"key\": \"value\"}, read it, report the key.",
]

def run_once(attempt):
    with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False, encoding="utf-8") as f:
        output_file = f.name
    stdout_fh = open(output_file, "w", encoding="utf-8")
    proc = subprocess.Popen(
        ["claude", "-p", "--output-format", "stream-json", "--verbose"],
        stdin=subprocess.PIPE, stdout=stdout_fh, stderr=subprocess.PIPE, text=True,
    )
    proc.stdin.write(PROMPTS[attempt % len(PROMPTS)])
    proc.stdin.close()

    start = time.monotonic()
    hung = False
    while time.monotonic() - start < TIMEOUT:
        try:
            proc.wait(timeout=2)
            break
        except subprocess.TimeoutExpired:
            if time.monotonic() - start > 20:
                try:
                    with open(output_file, "r", encoding="utf-8") as f:
                        content = f.read()
                except OSError:
                    continue
                if '"type":"assistant"' in content and '"type":"result"' not in content:
                    s1 = os.path.getsize(output_file)
                    time.sleep(5)
                    if s1 == os.path.getsize(output_file):
                        hung = True
                        break

    stdout_fh.close()
    elapsed = time.monotonic() - start
    if proc.poll() is None:
        proc.kill()
        proc.wait()

    with open(output_file, "r", encoding="utf-8") as f:
        content = f.read()
    has_result = '"type":"result"' in content
    has_assistant = '"type":"assistant"' in content
    events = []
    for line in content.strip().splitlines():
        if line.strip():
            try: events.append(json.loads(line).get("type", "?"))
            except: events.append("ERR")

    bug = has_assistant and not has_result and hung
    status = "HUNG" if proc.returncode is None or hung else f"exit:{proc.returncode}"
    marker = "*** BUG ***" if bug else "ok"
    print(f"  Run {attempt}: [{status}] {elapsed:.0f}s  result={has_result}  {marker}")
    if bug:
        print(f"    Last events: {' -> '.join(events[-6:])}")
    os.unlink(output_file)
    return bug

total = bugs = 0
for batch in range(5):
    print(f"--- Batch {batch+1}: {CONCURRENCY} concurrent processes ---")
    with concurrent.futures.ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
        futs = [pool.submit(run_once, batch * CONCURRENCY + i) for i in range(CONCURRENCY)]
        for f in concurrent.futures.as_completed(futs):
            total += 1
            if f.result(): bugs += 1
    if bugs > 0:
        print(f"\nBUG REPRODUCED: {bugs}/{total} runs hung")
        break
print(f"Total: {bugs}/{total} hangs")

Our output on v2.1.85 (Arch Linux):

--- Batch 1: 6 concurrent processes ---
  Run 1: [exit:0] 12s  result=True  ok
  Run 5: [exit:0] 12s  result=True  ok
  Run 4: [exit:0] 14s  result=True  ok
  Run 0: [exit:0] 15s  result=True  ok
  Run 3: [exit:0] 20s  result=True  ok
  Run 2: [exit:0] 26s  result=True  ok
--- Batch 2: 6 concurrent processes ---
  Run 7: [exit:0] 9s   result=True  ok
  Run 11: [exit:0] 13s  result=True  ok
  Run 8: [exit:0] 19s  result=True  ok
  Run 9: [exit:0] 19s  result=True  ok
  Run 10: [HUNG] 25s  result=False  *** BUG ***
    Last events: assistant -> rate_limit_event -> user -> assistant -> user
  Run 6: [HUNG] 32s  result=False  *** BUG ***
    Last events: user -> assistant -> assistant -> user

BUG REPRODUCED: 2/12 runs hung

Concurrency and tool-use prompts are key. Sequential runs with simple prompts may not reproduce.

Claude Model

Not sure / Multiple models

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.85 (Claude Code)

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

Non-interactive/CI environment

Additional Information

Prior issues (all auto-closed, not fixed)

| Issue | Description | Disposition |
|-------|-------------|-------------|
| #1920 | Missing result event in stream-json | Auto-closed (inactivity) |
| #21099 | claude -p never exits when stdout is piped | Auto-closed (inactivity) |
| #25629 | Hangs after result event in stream-json | Auto-closed (inactivity) |

TTY hypothesis

All reports share: stdout is not a TTY. Interactive mode works fine. This points to a race condition or deadlock in the non-TTY exit/cleanup path. Concurrency further increases the rate, suggesting shared resource contention.

Workaround

We monitor the output file for event markers and detect two stall patterns:

  1. Post-completion stall: last event is "type":"assistant" with no subsequent tool activity for 30s → force-kill (the final response was sent but result event never came)
  2. Mid-conversation stall: last event is an active marker ("type":"user" / tool_result) with no new output for 90s → force-kill (the CLI stopped mid-turn)

Both cases treat the missing result as a retryable error.

View original on GitHub ↗

This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗