[DOCS] Blocking `input()` usage in async `can_use_tool` callback freezes event loop in Python examples
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:
- Network keep-alives or heartbeats required by the SDK connection.
- 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_threadis 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.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗