Skip to content

can_use_tool callback never invoked without hooks/MCP servers: stdin closes before permission requests arrive ("Error: Stream closed") #1105

Description

@Echolonius

Symptom

With a can_use_tool callback and no hooks and no SDK MCP servers — the query() + AsyncIterable path that can_use_tool's own ValueError directs users to — the callback is never invoked. Every tool call that needs permission fails and the model burns turns retrying:

ToolResultBlock(content='Tool permission request failed: Error: Stream closed', is_error=True)

Mechanism

Query.wait_for_result_and_end_input (src/claude_agent_sdk/_internal/query.py:819) keeps stdin open until the first result only for SDK MCP servers and hooks:

if self.sdk_mcp_servers or self.hooks:
    ...
    await self._first_result_event.wait()

await self.transport.end_input()

can_use_tool is missing from the condition, but it is bidirectional in exactly the same way: the CLI sends a can_use_tool control_request mid-turn and the SDK must write the permission response back over stdin. With only can_use_tool configured, stream_input() reaches end_input() as soon as the input stream is exhausted — i.e. seconds before the turn's first tool call — so the control channel is gone by the time the CLI asks for permission.

The string-prompt path can't hit this (can_use_tool + string prompt raises ValueError in _process_query_inner), so the affected path is precisely the one the error message recommends.

Repro (real CLI, SDK 0.2.116, CLI 2.1.207)

One user message via an async generator, ClaudeAgentOptions(can_use_tool=...), prompt asks Claude to Write a file (permission-gated):

  • Case A — can_use_tool only: callback never fires; every Write attempt returns Tool permission request failed: Error: Stream closed; run ends in error_max_turns.
  • Case B — identical + one no-op hook whose matcher ("NoSuchToolEver") never fires: callback is invoked, PermissionResultAllow() is honored, file is created, run succeeds.

The only difference between A and B is that the hook's presence flips the keep-stdin-open condition — confirming the missing can_use_tool term is the root cause. (Repro script available on request; it's ~70 lines.)

Fix

Include can_use_tool in the condition:

if self.sdk_mcp_servers or self.hooks or self.can_use_tool:

PR with this fix + a regression test (mock transport that enforces the real CLI contract: permission request arrives after input is exhausted, result only after the response is written, writes after end_input() raise) is attached.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions