obsidian/wiki/agent-sdk/user-input-approvals.md
2026-04-17 12:50:20 +01:00

171 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 **14 questions**, each with **24 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