[DOCS] Blocking `input()` usage in async `can_use_tool` callback freezes event loop in Python examples

Resolved 💬 4 comments Opened Jan 21, 2026 by coygeek Closed Jan 24, 2026

Documentation Type

Unclear/confusing documentation

Documentation Location

https://platform.claude.com/docs/en/agent-sdk/user-input

Section/Topic

The "Handle tool approval requests" section, specifically the Python code example demonstrating the can_use_tool callback.

Current Documentation

The current Python example uses the synchronous input() function within an async callback:

async def can_use_tool(
    tool_name: str, input_data: dict, context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
    # Display the tool request
    print(f"\nTool: {tool_name}")
    # ... (omitted print logic)

    # Get user approval
    response = input("Allow this action? (y/n): ")  # <--- BLOCKING CALL

    # Return allow or deny based on user's response
    if response.lower() == "y":
        return PermissionResultAllow(updated_input=input_data)
    else:
        return PermissionResultDeny(message="User denied this action")

What's Wrong or Missing?

The example defines can_use_tool as an async def, which means it runs on the asyncio event loop. However, it uses the standard Python input() function, which is synchronous and blocking.

When input() is waiting for the user to type 'y' or 'n', it completely blocks the Python event loop. This means no other background tasks can run, including:

  1. Network keep-alives or heartbeats required by the SDK connection.
  2. Any concurrent tasks or streaming processing the application might be attempting to handle.

In a production or complex agent scenario, this blocking behavior can lead to connection timeouts or unresponsive behavior if the user does not reply instantly.

Suggested Improvement

The example should use asyncio.to_thread (available in Python 3.9+, which fits the SDK's 3.10+ requirement) to run the blocking input in a separate thread, allowing the event loop to continue running while waiting for user interaction.

Suggested replacement code:

import asyncio
# ... imports ...

async def can_use_tool(
    tool_name: str, input_data: dict, context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
    # ... print logic ...

    # Get user approval in a non-blocking way
    # input() is blocking, so we run it in a separate thread to keep the event loop alive
    response = await asyncio.to_thread(input, "Allow this action? (y/n): ")

    if response.lower() == "y":
        return PermissionResultAllow(updated_input=input_data)
    else:
        return PermissionResultDeny(message="User denied this action")

Impact

High - Prevents users from using a feature

Additional Context

  • The SDK requires Python 3.10+, so asyncio.to_thread is guaranteed to be available.
  • Using blocking I/O in async functions is a common antipattern that new users copy-pasting from documentation might not realize causes side effects in larger applications.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗