Status: workaround in place locally; would benefit from upstream fix.
Reproduced on: macOS 25.4.0 (Darwin), claude-agent-sdk 0.1.x, 2026-05-05.
Affected use case: anything that captures the SDK's output (orchestrators, CI runners, headless launchd / cron jobs, Docker without -t).
Symptom
Any Python script that uses claude_agent_sdk hangs indefinitely when its stdout is a pipe or a regular file (i.e., not a terminal). Once the script hits the first query() call, the process goes to a sleeping state, makes no progress, and consumes near-zero CPU. No output is ever emitted. The hang persists past arbitrary timeouts.
The same script, run with stdout connected to a TTY, completes normally in expected time.
Minimal repro
```python
tty_bug.py
import asyncio
from claude_agent_sdk import (
AssistantMessage, ClaudeAgentOptions, ResultMessage, TextBlock, query,
)
async def main():
options = ClaudeAgentOptions(
system_prompt="Reply with exactly one word.",
allowed_tools=[],
thinking={"type": "adaptive"},
effort="low",
permission_mode="bypassPermissions",
model="claude-opus-4-7",
setting_sources=None,
)
parts = []
async for msg in query(prompt="Say 'hello'.", options=options):
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
parts.append(block.text)
elif isinstance(msg, ResultMessage):
pass
print("response:", "".join(parts).strip())
asyncio.run(main())
```
| Invocation |
Result |
| `python tty_bug.py` |
Completes in ~2s |
| `python tty_bug.py > out.txt` |
Hangs indefinitely. Kill required. `out.txt` empty. |
| `python tty_bug.py | cat` |
Hangs indefinitely. Kill required. |
| `script -q /dev/null python tty_bug.py > out.txt` |
Completes; output written to file |
`script -q` allocates a real PTY, which is then forwarded to its own stdout. The bundled `claude` binary inside claude-agent-sdk sees a TTY and proceeds normally.
Why this matters
This bug bites anyone running the SDK in:
- An orchestrator / pipeline that captures step output (subprocess.PIPE).
- CI / GitHub Actions runners — they pipe stdout to log aggregators.
- launchd / cron jobs — no controlling terminal, stdout redirected to a file via the agent's plist or crontab.
- Docker containers without `-t`.
In all four cases the symptom is identical: silent hang, no diagnostic output, no error.
A real-world impact data point: a launchd-driven nightly pipeline saw analyze steps time out at 1800s every single tick for 24+ hours. Diagnosis required adding custom logging to the SDK consumer to discover the stream was hanging silently after a `RateLimitEvent` was yielded but no further message arrived.
Suspected mechanism
The bundled `claude` binary at `/lib/python3.x/site-packages/claude_agent_sdk/_bundled/claude` appears to do an `isatty()` check on stdout and behaves differently when not connected to a terminal. Diagnostic confirmation:
- `script -q /dev/null` (which gives a PTY) consistently fixes it.
- Setting `PYTHONUNBUFFERED=1` does NOT fix it (so it's not a Python-side stdout buffering issue — it's inside the bundled binary).
- Disconnecting stdin (`< /dev/null`) does NOT fix it.
Additional issue: when an Anthropic-side rate limit fires, the SDK yields a `RateLimitEvent` message and then the stream hangs indefinitely instead of erroring or auto-retrying. Combined with the TTY-required behavior, this means a launchd-style consumer sees no output, no error, no progress — just a 1800s timeout.
Workaround
Wrap the entire SDK-using process in `script -q /dev/null`, OR detect `RateLimitEvent` in the iterator and abort with a retryable error so the caller's retry-with-backoff handles it.
For an orchestrator using `asyncio.create_subprocess_exec`, prepend the wrapper to the command:
```python
cmd = ["/usr/bin/script", "-q", "/dev/null", *original_cmd]
proc = await asyncio.create_subprocess_exec(*cmd, ...)
```
`script` syntax differs between macOS and Linux:
- macOS: `script -q <cmd...>`
- Linux: `script -q -c "" `
Real fix (upstream)
Either:
- Don't `isatty()` to switch behavior. The SDK should produce the same output regardless of terminal attachment.
- If terminal-only behavior (progress spinners, ANSI colors) is genuinely useful when interactive, gate it on a `--non-interactive` flag, defaulting to non-interactive when `not isatty(stdout)`.
- Surface `RateLimitEvent` as a retryable Python exception that terminates the iterator, rather than as a continuing stream that may hang.
Status: workaround in place locally; would benefit from upstream fix.
Reproduced on: macOS 25.4.0 (Darwin), claude-agent-sdk 0.1.x, 2026-05-05.
Affected use case: anything that captures the SDK's output (orchestrators, CI runners, headless launchd / cron jobs, Docker without
-t).Symptom
Any Python script that uses
claude_agent_sdkhangs indefinitely when its stdout is a pipe or a regular file (i.e., not a terminal). Once the script hits the firstquery()call, the process goes to a sleeping state, makes no progress, and consumes near-zero CPU. No output is ever emitted. The hang persists past arbitrary timeouts.The same script, run with stdout connected to a TTY, completes normally in expected time.
Minimal repro
```python
tty_bug.py
import asyncio
from claude_agent_sdk import (
AssistantMessage, ClaudeAgentOptions, ResultMessage, TextBlock, query,
)
async def main():
options = ClaudeAgentOptions(
system_prompt="Reply with exactly one word.",
allowed_tools=[],
thinking={"type": "adaptive"},
effort="low",
permission_mode="bypassPermissions",
model="claude-opus-4-7",
setting_sources=None,
)
parts = []
async for msg in query(prompt="Say 'hello'.", options=options):
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
parts.append(block.text)
elif isinstance(msg, ResultMessage):
pass
print("response:", "".join(parts).strip())
asyncio.run(main())
```
`script -q` allocates a real PTY, which is then forwarded to its own stdout. The bundled `claude` binary inside claude-agent-sdk sees a TTY and proceeds normally.
Why this matters
This bug bites anyone running the SDK in:
In all four cases the symptom is identical: silent hang, no diagnostic output, no error.
A real-world impact data point: a launchd-driven nightly pipeline saw analyze steps time out at 1800s every single tick for 24+ hours. Diagnosis required adding custom logging to the SDK consumer to discover the stream was hanging silently after a `RateLimitEvent` was yielded but no further message arrived.
Suspected mechanism
The bundled `claude` binary at `/lib/python3.x/site-packages/claude_agent_sdk/_bundled/claude` appears to do an `isatty()` check on stdout and behaves differently when not connected to a terminal. Diagnostic confirmation:
Additional issue: when an Anthropic-side rate limit fires, the SDK yields a `RateLimitEvent` message and then the stream hangs indefinitely instead of erroring or auto-retrying. Combined with the TTY-required behavior, this means a launchd-style consumer sees no output, no error, no progress — just a 1800s timeout.
Workaround
Wrap the entire SDK-using process in `script -q /dev/null`, OR detect `RateLimitEvent` in the iterator and abort with a retryable error so the caller's retry-with-backoff handles it.
For an orchestrator using `asyncio.create_subprocess_exec`, prepend the wrapper to the command:
```python
cmd = ["/usr/bin/script", "-q", "/dev/null", *original_cmd]
proc = await asyncio.create_subprocess_exec(*cmd, ...)
```
`script` syntax differs between macOS and Linux:
Real fix (upstream)
Either: