171 lines
6.2 KiB
Markdown
171 lines
6.2 KiB
Markdown
---
|
||
title: "Handle Approvals and User Input"
|
||
aliases: [canUseTool, user-approvals, ask-user-question]
|
||
tags: [agent-sdk, permissions, user-input, python, typescript]
|
||
sources: [raw/Handle approvals and user input.md]
|
||
created: 2026-04-17
|
||
updated: 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\|permission rules]] |
|
||
| 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
|
||
|
||
```python
|
||
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.
|
||
|
||
```python
|
||
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\|plan mode]]), it calls `AskUserQuestion`. Route on `tool_name == "AskUserQuestion"`.
|
||
|
||
### Input format Claude sends
|
||
|
||
```json
|
||
{
|
||
"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
|
||
|
||
```json
|
||
{
|
||
"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
|
||
|
||
```python
|
||
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\|Hooks]] | 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\|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\|hooks]] instead of `canUseTool`
|
||
|
||
## Sources
|
||
|
||
- `raw/Handle approvals and user input.md` — official Agent SDK docs on user input handling
|