Chapter 13
Handle Approvals and User Input
Surface Claude's approval requests and clarifying questions to users, then return their decisions to the SDK.
While working on a task, Claude sometimes needs to check in with users. It might need permission before deleting files, or need to ask which database to use for a new project. Your application needs to surface these requests to users so Claude can continue with their input.
Claude requests user input in two situations: when it needs permission to use a tool (like deleting files or running commands), and when it has clarifying questions (via the AskUserQuestion tool). Both trigger your canUseTool callback, which pauses execution until you return a response.
For clarifying questions, Claude generates the questions and options. Your role is to present them to users and return their selections. You can't add your own questions to this flow; if you need to ask users something yourself, do that separately in your application logic.
Detect when Claude needs input
Pass a canUseTool callback in your query options. The callback fires whenever Claude needs user input, receiving the tool name and input as arguments:
async function handleToolRequest(toolName, input) {
// Prompt user and return allow or deny
}
const options = { canUseTool: handleToolRequest }The callback fires in two cases:
- Tool needs approval: Claude wants to use a tool that isn't auto-approved by permission rules or modes. Check
toolNamefor the tool (e.g.,"Bash","Write"). - Claude asks a question: Claude calls the
AskUserQuestiontool. Check iftoolName == "AskUserQuestion"to handle it differently.
Your callback must return within 60 seconds or Claude will assume the request was denied and try a different approach.
Handle tool approval requests
Once you've passed a canUseTool callback, it fires when Claude wants to use a tool that isn't auto-approved. Your callback receives:
| Argument | Description |
|---|---|
toolName | The name of the tool Claude wants to use (e.g., "Bash", "Write", "Edit") |
input | The parameters Claude is passing to the tool. Contents vary by tool. |
Common input fields by tool:
| Tool | Input fields |
|---|---|
Bash | command, description, timeout |
Write | file_path, content |
Edit | file_path, old_string, new_string |
Read | file_path, offset, limit |
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Create a test file in /tmp and then delete it",
options: {
canUseTool: async (toolName, input) => {
// Display the tool request
console.log(`Tool: ${toolName}`);
if (toolName === "Bash") {
console.log(`Command: ${input.command}`);
}
// Get user approval
const response = await prompt("Allow this action? (y/n): ");
if (response.toLowerCase() === "y") {
return { behavior: "allow", updatedInput: input };
}
return { behavior: "deny", message: "User denied this action" };
}
}
})) {
if ("result" in message) console.log(message.result);
}Respond to tool requests
Your callback returns one of two response types:
| Response | TypeScript |
|---|---|
| Allow | { behavior: "allow", updatedInput } |
| Deny | { behavior: "deny", message } |
When allowing, pass the tool input (original or modified). When denying, provide a message explaining why. Claude sees this message and may adjust its approach.
Approve
Let the tool execute as Claude requested.
return { behavior: "allow", updatedInput: input };Approve with changes
Modify the input before execution (e.g., sanitize paths, add constraints).
const sandboxedInput = {
...input,
command: input.command.replace("/tmp", "/tmp/sandbox")
};
return { behavior: "allow", updatedInput: sandboxedInput };Reject
Block the tool and tell Claude why.
return { behavior: "deny", message: "User rejected this action" };Suggest alternative
Block but guide Claude toward what the user wants instead.
return {
behavior: "deny",
message: "User doesn't want to delete files. " +
"They asked if you could compress them into an archive instead."
};Handle clarifying questions
When Claude needs more direction on a task with multiple valid approaches, it calls the AskUserQuestion tool. This triggers your canUseTool callback with toolName set to AskUserQuestion. The input contains Claude's questions as multiple-choice options, which you display to the user and return their selections.
Tip
Clarifying questions are especially common in plan mode, where Claude explores the codebase and asks questions before proposing a plan. This makes plan mode ideal for interactive workflows where you want Claude to gather requirements before making changes.
Detect AskUserQuestion
Check if toolName === "AskUserQuestion" to handle it differently from other tools
Parse the question input
The input contains a questions array with question, options, and multiSelect fields
Collect answers from the user
Present the questions to your users and collect their selections
Return answers to Claude
Build an answers object mapping each question to the selected label
Question format
The input contains Claude's generated questions in a questions array. Each question has these fields:
| Field | Description |
|---|---|
question | The full question text to display |
header | Short label for the question (max 12 characters) |
options | Array of 2-4 choices, each with label and description |
multiSelect | If true, users can select multiple options |
{
"questions": [
{
"question": "How should I format the output?",
"header": "Format",
"options": [
{ "label": "Summary", "description": "Brief overview" },
{ "label": "Detailed", "description": "Full explanation" }
],
"multiSelect": false
}
]
}Response format
Return an answers object mapping each question's question field to the selected option's label:
return {
behavior: "allow",
updatedInput: {
questions: input.questions,
answers: {
"How should I format the output?": "Summary",
"Which sections should I include?": "Introduction, Conclusion"
}
}
};For multi-select questions, join multiple labels with ", ". For free-text input, use the user's custom text directly.
Complete example
This example handles clarifying questions in a terminal application. Claude asks questions when it needs user input to proceed, such as when deciding on a tech stack for a mobile app.
import { query } from "@anthropic-ai/claude-agent-sdk";
// Parse user input as option number(s) or free text
function parseResponse(response: string, options: any[]): string {
const indices = response.split(",").map((s) => parseInt(s.trim()) - 1);
const labels = indices
.filter((i) => !isNaN(i) && i >= 0 && i < options.length)
.map((i) => options[i].label);
return labels.length > 0 ? labels.join(", ") : response;
}
// Display questions and collect answers
async function handleAskUserQuestion(input: any) {
const answers: Record<string, string> = {};
for (const q of input.questions) {
console.log(`
${q.header}: ${q.question}`);
q.options.forEach((opt, i) => {
console.log(` ${i + 1}. ${opt.label} - ${opt.description}`);
});
const response = (await prompt("Your choice: ")).trim();
answers[q.question] = parseResponse(response, q.options);
}
return {
behavior: "allow",
updatedInput: { questions: input.questions, answers }
};
}Limitations
- -60-second timeout:
canUseToolcallbacks must return within 60 seconds or Claude will retry with a different approach - -Subagents:
AskUserQuestionis not currently available in subagents spawned via the Task tool - -Question limits: each
AskUserQuestioncall supports 1-4 questions with 2-4 options each
Other ways to get user input
The canUseTool callback and AskUserQuestion tool cover most approval and clarification scenarios, but the SDK offers other ways to get input:
Streaming input
Use when you need to:
- Interrupt the agent mid-task
- Provide additional context without waiting for Claude to ask
- Build chat interfaces with follow-up messages
Custom tools
Use when you need to:
- Collect structured input beyond multiple-choice
- Integrate external approval systems
- Implement domain-specific interactions
Related resources
- -Configure permissions: set up permission modes and rules
- -Control execution with hooks: run custom code at key points in the agent lifecycle