Description
When using query() with a string prompt and SDK hooks (e.g., PreToolUse), the SDK closes stdin after a 60-second timeout via wait_for_result_and_end_input(), even if the CLI is still actively processing and sending hook callback requests. This kills the bidirectional control protocol, causing the CLI to fail with:
Error in hook callback hook_0: ... Tool permission stream closed before response received
The error then propagates as "unhandled errors in a TaskGroup (1 sub-exception)".
Root Cause
In claude_agent_sdk/_internal/query.py, wait_for_result_and_end_input() waits for the first type: "result" message with a default timeout of 60 seconds (CLAUDE_CODE_STREAM_CLOSE_TIMEOUT). Since the "result" message only arrives when the entire conversation completes, any agent session with hooks that runs longer than 60 seconds will have stdin closed prematurely:
async def wait_for_result_and_end_input(self) -> None:
if self.sdk_mcp_servers or self.hooks:
with anyio.move_on_after(self._stream_close_timeout): # default: 60s
await self._first_result_event.wait()
await self.transport.end_input() # closes stdin regardless of timeout
After stdin closes, the CLI can no longer send hook callback requests, and any in-flight hook callback fails with "Tool permission stream closed before response received".
Minimal Reproducible Example
"""MRE: SDK closes stdin after 60s, killing hook callbacks.
Run with: uv run python mre.py
Requires: claude-agent-sdk, claude CLI installed
"""
import asyncio
from claude_agent_sdk import (
ClaudeAgentOptions,
HookContext,
HookInput,
HookJSONOutput,
HookMatcher,
query,
)
async def my_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Simple PreToolUse hook that allows everything."""
print(f"[hook] tool={input_data.get('tool_name', '?')}")
return {} # no opinion — pass through
async def main() -> None:
options = ClaudeAgentOptions(
permission_mode="bypassPermissions",
hooks={
"PreToolUse": [
HookMatcher(
matcher="Bash",
hooks=[my_hook],
)
]
},
)
# This prompt forces the agent to work for >60s, triggering many Bash
# tool calls (each invoking the hook). After 60s the SDK closes stdin
# and the next hook callback fails.
prompt = (
"Search this entire repository recursively for every Python file. "
"For each file, read it and count the lines. "
"Then produce a summary table of all files and their line counts. "
"Use the Bash tool with 'find' and 'wc' commands. "
"Do this file by file, not all at once."
)
try:
async for msg in query(prompt=prompt, options=options):
msg_type = getattr(msg, "type", None) or type(msg).__name__
print(f"[msg] {msg_type}")
except Exception as e:
print(f"\n[ERROR] {e}")
# Expected after ~60s:
# "unhandled errors in a TaskGroup (1 sub-exception)"
# CLI stderr will show:
# "Error in hook callback hook_0: ... Tool permission stream closed
# before response received"
if __name__ == "__main__":
asyncio.run(main())
Expected Behavior
The SDK should keep stdin open for the entire duration of the conversation when hooks (or SDK MCP servers) are active, since they require the bidirectional control protocol.
Actual Behavior
After 60 seconds (the default CLAUDE_CODE_STREAM_CLOSE_TIMEOUT), stdin is closed regardless of whether the conversation is still active, breaking all subsequent hook callbacks.
Workaround
Set a very large timeout via environment variable:
export CLAUDE_CODE_STREAM_CLOSE_TIMEOUT=3600000 # 1 hour in ms
Suggested Fix
When hooks or SDK MCP servers are present, wait_for_result_and_end_input() should not apply a timeout — stdin should remain open until the first result arrives or the CLI process exits:
async def wait_for_result_and_end_input(self) -> None:
if self.sdk_mcp_servers or self.hooks:
# No timeout — stdin must stay open for the control protocol
await self._first_result_event.wait()
await self.transport.end_input()
Environment
claude-agent-sdk version: 0.1.50 (bundled CLI 2.1.81)
- Python: 3.13
- OS: macOS (Darwin 25.3.0)
anyio used as the async runtime
Description
When using
query()with a string prompt and SDK hooks (e.g.,PreToolUse), the SDK closes stdin after a 60-second timeout viawait_for_result_and_end_input(), even if the CLI is still actively processing and sending hook callback requests. This kills the bidirectional control protocol, causing the CLI to fail with:The error then propagates as
"unhandled errors in a TaskGroup (1 sub-exception)".Root Cause
In
claude_agent_sdk/_internal/query.py,wait_for_result_and_end_input()waits for the firsttype: "result"message with a default timeout of 60 seconds (CLAUDE_CODE_STREAM_CLOSE_TIMEOUT). Since the "result" message only arrives when the entire conversation completes, any agent session with hooks that runs longer than 60 seconds will have stdin closed prematurely:After stdin closes, the CLI can no longer send hook callback requests, and any in-flight hook callback fails with "Tool permission stream closed before response received".
Minimal Reproducible Example
Expected Behavior
The SDK should keep stdin open for the entire duration of the conversation when hooks (or SDK MCP servers) are active, since they require the bidirectional control protocol.
Actual Behavior
After 60 seconds (the default
CLAUDE_CODE_STREAM_CLOSE_TIMEOUT), stdin is closed regardless of whether the conversation is still active, breaking all subsequent hook callbacks.Workaround
Set a very large timeout via environment variable:
Suggested Fix
When hooks or SDK MCP servers are present,
wait_for_result_and_end_input()should not apply a timeout — stdin should remain open until the first result arrives or the CLI process exits:Environment
claude-agent-sdkversion: 0.1.50 (bundled CLI 2.1.81)anyioused as the async runtime