| title |
aliases |
tags |
sources |
created |
updated |
| Handle Approvals and User Input |
| canUseTool |
| user-approvals |
| ask-user-question |
|
| agent-sdk |
| permissions |
| user-input |
| python |
| typescript |
|
| raw/Handle approvals and user input.md |
|
2026-04-17 |
2026-04-17 |
Handle Approvals and User Input
Surface Claude's approval requests and clarifying questions to users, then return their decisions back to the SDK via the canUseTool callback.
When Claude Pauses for Input
Claude stops and fires canUseTool in two situations:
| Situation |
tool_name value |
Trigger |
| Tool needs permission |
"Bash", "Write", "Edit", etc. |
Tool not auto-approved by wiki/agent-sdk/configure-permissions |
| Clarifying question |
"AskUserQuestion" |
Claude needs direction before proceeding |
This is distinct from a normal conversation turn — execution is paused until your callback returns.
canUseTool Callback Setup
async def handle_tool_request(tool_name, input_data, context):
# prompt user, return allow or deny
...
options = ClaudeAgentOptions(can_use_tool=handle_tool_request)
Python requirement: streaming mode + a PreToolUse hook returning {"continue_": True} — without it, the stream closes before the callback fires.
async def dummy_hook(input_data, tool_use_id, context):
return {"continue_": True}
options = ClaudeAgentOptions(
can_use_tool=can_use_tool,
hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
)
Tool Approval Requests
Callback arguments
| Argument |
Description |
tool_name |
Tool Claude wants to use ("Bash", "Write", "Edit", "Read") |
input_data |
Tool-specific parameters (e.g. command, file_path, content) |
context |
Suggestions (PermissionUpdate entries) + cancellation signal |
Response types
| Response |
Python |
TypeScript |
| Allow |
PermissionResultAllow(updated_input=input_data) |
{ behavior: "allow", updatedInput } |
| Deny |
PermissionResultDeny(message="reason") |
{ behavior: "deny", message } |
Response strategies
- Approve — pass input unchanged
- Approve with changes — sanitize paths, add constraints before passing
- Reject — block the tool, Claude sees your message and may adjust
- Suggest alternative — block + guide Claude toward what user wants
- Redirect — use streaming input to send a new instruction entirely
AskUserQuestion — Clarifying Questions
When Claude needs direction (common in wiki/agent-sdk/configure-permissions), it calls AskUserQuestion. Route on tool_name == "AskUserQuestion".
Input format Claude sends
{
"questions": [
{
"question": "How should I format the output?",
"header": "Format",
"options": [
{ "label": "Summary", "description": "Brief overview of key points" },
{ "label": "Detailed", "description": "Full explanation with examples" }
],
"multiSelect": false
}
]
}
Response format you return
{
"questions": [ /* pass through original */ ],
"answers": {
"How should I format the output?": "Summary",
"Which sections should I include?": "Introduction, Conclusion"
}
}
- Single-select: one label
- Multi-select: labels joined with
", "
- Free text: user's raw text (add an "Other" option in your UI)
Option previews (TypeScript only)
Set toolConfig.askUserQuestion.previewFormat to "markdown" or "html" — Claude adds a preview field to options where a visual comparison helps. Check for undefined before rendering.
Complete Python Example
def parse_response(response: str, options: list) -> str:
try:
indices = [int(s.strip()) - 1 for s in response.split(",")]
labels = [options[i]["label"] for i in indices if 0 <= i < len(options)]
return ", ".join(labels) if labels else response
except ValueError:
return response
async def handle_ask_user_question(input_data: dict) -> PermissionResultAllow:
answers = {}
for q in input_data.get("questions", []):
print(f"\n{q['header']}: {q['question']}")
for i, opt in enumerate(q["options"]):
print(f" {i+1}. {opt['label']} - {opt['description']}")
response = input("Your choice: ").strip()
answers[q["question"]] = parse_response(response, q["options"])
return PermissionResultAllow(
updated_input={"questions": input_data.get("questions", []), "answers": answers}
)
async def can_use_tool(tool_name, input_data, context):
if tool_name == "AskUserQuestion":
return await handle_ask_user_question(input_data)
# display tool info, prompt y/n
response = input(f"Allow {tool_name}? (y/n): ")
if response.lower() == "y":
return PermissionResultAllow(updated_input=input_data)
return PermissionResultDeny(message="User denied this action")
Limitations
AskUserQuestion is not available in subagents spawned via the Agent tool
- Each call supports 1–4 questions, each with 2–4 options
Alternatives to canUseTool
| Mechanism |
Best for |
| wiki/agent-sdk/hooks-guide |
Auto-allow/deny without prompting; external notifications (Slack, email) via PermissionRequest hook |
| Streaming input |
Interrupting mid-task, providing context, chat interfaces |
| wiki/agent-sdk/custom-tools |
Structured forms, multi-step workflows, external approval systems |
Key Takeaways
canUseTool is the single callback for both tool approvals and AskUserQuestion — branch on tool_name
- Python requires streaming mode + a
PreToolUse dummy hook to keep the stream alive
- Deny responses include a message Claude reads — use it to redirect, not just block
AskUserQuestion is especially common in plan mode; include it in your tools array if you define one
- You can modify tool input before allowing (sanitize, constrain) — not just pass-through
- For automation without user prompts, use wiki/agent-sdk/hooks-guide instead of
canUseTool
Sources
raw/Handle approvals and user input.md — official Agent SDK docs on user input handling