SDK version: 0.2.115 · CLI: 2.1.206 · Platform: macOS (darwin)
Bug
When the claude subprocess dies mid-run (observed repeatedly when a Pro/Max
subscription hits its rate-limit window and the CLI exits with code 1), the
SDK logs Fatal error in message reader: Command failed with exit code 1
but the query() async stream neither terminates nor raises — a consumer
awaiting the next message waits forever.
Repro sketch
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, query
async def main():
stream = query(
prompt="Write a long essay.",
options=ClaudeAgentOptions(model="claude-haiku-4-5", tools=[], max_turns=1),
)
async for msg in stream: # kill the spawned `claude` process mid-generation:
print(type(msg).__name__) # pkill -f "claude.*stream-json"
print("stream ended") # <- never reached; no exception either
asyncio.run(main())
Killing the subprocess externally reproduces it deterministically; the
organic trigger we hit was subscription rate-limit exhaustion.
Expected
Process death should end the stream: either raise ProcessError (ideal, with
the exit code) or terminate the iteration, so consumers can react instead of
hanging. receive_messages()'s internal reader appears to catch the fatal
error without closing the memory channel the consumer is blocked on.
Impact / workaround
Any consumer without an explicit timeout hangs indefinitely; timeouts that
cancel the iteration can additionally leak the transport if cleanup tasks are
scheduled on a loop that closes right after. We work around it in
langchain-claude-cli with a per-message inactivity watchdog plus an explicit
stream.aclose() executed inside the still-running loop.
SDK version: 0.2.115 · CLI: 2.1.206 · Platform: macOS (darwin)
Bug
When the
claudesubprocess dies mid-run (observed repeatedly when a Pro/Maxsubscription hits its rate-limit window and the CLI exits with code 1), the
SDK logs
Fatal error in message reader: Command failed with exit code 1but the
query()async stream neither terminates nor raises — a consumerawaiting the next message waits forever.
Repro sketch
Killing the subprocess externally reproduces it deterministically; the
organic trigger we hit was subscription rate-limit exhaustion.
Expected
Process death should end the stream: either raise
ProcessError(ideal, withthe exit code) or terminate the iteration, so consumers can react instead of
hanging.
receive_messages()'s internal reader appears to catch the fatalerror without closing the memory channel the consumer is blocked on.
Impact / workaround
Any consumer without an explicit timeout hangs indefinitely; timeouts that
cancel the iteration can additionally leak the transport if cleanup tasks are
scheduled on a loop that closes right after. We work around it in
langchain-claude-cli with a per-message inactivity watchdog plus an explicit
stream.aclose()executed inside the still-running loop.