Skip to content

Commit 2c4f3e6

Browse files
declan-scaleclaude
andcommitted
fix(claude-code): drain stderr in tutorial _spawn_claude to avoid deadlock [greptile]
All three claude-code tutorial spawn helpers pass --verbose and capture stderr via asyncio.subprocess.PIPE but never read it. A non-trivial session can write enough verbose output to fill the OS pipe buffer, at which point Claude Code blocks on its stderr write while we block reading stdout — a hard deadlock. Drain stderr in a concurrent background task (awaited after proc.wait()) so stdout never stalls. Applied to 060_claude_code (sync), 130_claude_code (async base), and 140_claude_code (temporal). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0d2d301 commit 2c4f3e6

3 files changed

Lines changed: 36 additions & 0 deletions

File tree

  • examples/tutorials
    • 00_sync/060_claude_code/project
    • 10_async

examples/tutorials/00_sync/060_claude_code/project/acp.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ async def _spawn_claude(prompt: str) -> AsyncIterator[str]:
6868
proc.stdin.write(prompt.encode())
6969
proc.stdin.close()
7070

71+
# Drain stderr concurrently. With --verbose, Claude Code can write enough to
72+
# stderr to fill the OS pipe buffer; if we only read stdout, the CLI blocks
73+
# on its stderr write while we block reading stdout — a deadlock. A
74+
# background task keeps stderr flowing so stdout never stalls.
75+
async def _drain_stderr() -> None:
76+
assert proc.stderr is not None
77+
async for _ in proc.stderr:
78+
pass
79+
80+
stderr_task = asyncio.create_task(_drain_stderr())
81+
7182
buffer = ""
7283
async for chunk in proc.stdout:
7384
buffer += chunk.decode("utf-8", errors="replace")
@@ -81,6 +92,7 @@ async def _spawn_claude(prompt: str) -> AsyncIterator[str]:
8192
yield buffer.strip()
8293

8394
await proc.wait()
95+
await stderr_task
8496

8597

8698
@acp.on_message_send

examples/tutorials/10_async/00_base/130_claude_code/project/acp.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,17 @@ async def _spawn_claude(prompt: str) -> AsyncIterator[str]:
6969
proc.stdin.write(prompt.encode())
7070
proc.stdin.close()
7171

72+
# Drain stderr concurrently. With --verbose, Claude Code can write enough to
73+
# stderr to fill the OS pipe buffer; if we only read stdout, the CLI blocks
74+
# on its stderr write while we block reading stdout — a deadlock. A
75+
# background task keeps stderr flowing so stdout never stalls.
76+
async def _drain_stderr() -> None:
77+
assert proc.stderr is not None
78+
async for _ in proc.stderr:
79+
pass
80+
81+
stderr_task = asyncio.create_task(_drain_stderr())
82+
7283
buffer = ""
7384
async for chunk in proc.stdout:
7485
buffer += chunk.decode("utf-8", errors="replace")
@@ -82,6 +93,7 @@ async def _spawn_claude(prompt: str) -> AsyncIterator[str]:
8293
yield buffer.strip()
8394

8495
await proc.wait()
96+
await stderr_task
8597

8698

8799
@acp.on_task_create

examples/tutorials/10_async/10_temporal/140_claude_code/project/workflow.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,17 @@ async def _spawn_claude(prompt: str, session_id: str | None = None) -> AsyncIter
8686
proc.stdin.write(prompt.encode())
8787
proc.stdin.close()
8888

89+
# Drain stderr concurrently. With --verbose, Claude Code can write enough to
90+
# stderr to fill the OS pipe buffer; if we only read stdout, the CLI blocks
91+
# on its stderr write while we block reading stdout — a deadlock. A
92+
# background task keeps stderr flowing so stdout never stalls.
93+
async def _drain_stderr() -> None:
94+
assert proc.stderr is not None
95+
async for _ in proc.stderr:
96+
pass
97+
98+
stderr_task = asyncio.create_task(_drain_stderr())
99+
89100
buffer = ""
90101
async for chunk in proc.stdout:
91102
buffer += chunk.decode("utf-8", errors="replace")
@@ -99,6 +110,7 @@ async def _spawn_claude(prompt: str, session_id: str | None = None) -> AsyncIter
99110
yield buffer.strip()
100111

101112
await proc.wait()
113+
await stderr_task
102114

103115

104116
@workflow.defn(name=environment_variables.WORKFLOW_NAME)

0 commit comments

Comments
 (0)