Handle approvals and user input changedagent-sdk/user-input
Nearest release: v2.1.295, published 9 hours before upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.
Upstream edited this page at 9 Oct 2026 03:28 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 20 Oct 2026 18:37 UTC.
Upstream edited
Recorded here
Lines+576added
Lines−602removed
From line
16
where the diff opens
First seen
14 Aug 2026
this site's first read of the page
Recorded edits12to this page, all time
The whole hunk
from line 16, old and new numbered
/
The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
from line 16
1616
1717Pass a `canUseTool` callback in your query options. The callback fires whenever Claude needs user input, receiving the tool name and input as arguments:
1818
19<CodeGroup>
20 ```python Python theme={null}
21 from claude_agent_sdk import ClaudeAgentOptions
22
23
24 async def handle_tool_request(tool_name, input_data, context):
25 # Prompt user and return allow or deny
26 ...
27
28
29 options = ClaudeAgentOptions(can_use_tool=handle_tool_request)
30 ```
31
32 ```typescript TypeScript theme={null}
33 import type { CanUseTool } from "@anthropic-ai/claude-agent-sdk";
34
35 const handleToolRequest: CanUseTool = async (toolName, input, options) => {
36 // options includes { signal: AbortSignal, suggestions?: PermissionUpdate[] }
37 // Prompt the user here, then return allow or deny
38 return { behavior: "deny", message: "User declined" };
39 };
40
41 const options = { canUseTool: handleToolRequest };
42 ```
43</CodeGroup>
19```python Python theme={null}
20from claude_agent_sdk import ClaudeAgentOptions
21
22
23async def handle_tool_request(tool_name, input_data, context):
24 # Prompt user and return allow or deny
25 ...
26
27
28options = ClaudeAgentOptions(can_use_tool=handle_tool_request)
29```
30
31```typescript TypeScript theme={null}
32import type { CanUseTool } from "@anthropic-ai/claude-agent-sdk";
33
34const handleToolRequest: CanUseTool = async (toolName, input, options) => {
35 // options includes { signal: AbortSignal, suggestions?: PermissionUpdate[] }
36 // Prompt the user here, then return allow or deny
37 return { behavior: "deny", message: "User declined" };
38};
39
40const options = { canUseTool: handleToolRequest };
41```
4442
4543The callback fires in two cases:
4644
47451. **Tool needs approval**: Claude wants to use a tool that isn't auto-approved by a [permission rule](/docs/en/agent-sdk/permissions) or permission mode. Check `tool_name` for the tool (e.g., `"Bash"`, `"Write"`).
48462. **Claude asks a question**: Claude calls the `AskUserQuestion` tool. Check if `tool_name == "AskUserQuestion"` to handle it differently. If you specify a `tools` array, include `AskUserQuestion` for this to work. See [Handle clarifying questions](#handle-clarifying-questions) for details.
4947
50<Warning>
51 **The callback never fires for auto-approved tools.** Any approval earlier in the [permission evaluation flow](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated), an allow rule or a mode like `acceptEdits` or `bypassPermissions`, resolves the call before `canUseTool` is consulted. If you list a tool bare in `allowed_tools`, a `canUseTool` check for that tool runs only when the [evaluation flow](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated) routes the call back to a prompt, such as an ask rule or `plan` mode. For logic that must apply to every tool call, use a [`PreToolUse` hook](/docs/en/agent-sdk/hooks), which executes before the rest of the flow and can allow, deny, or modify requests.
52
53 An allow rule doesn't pre-approve the [actions no mode auto-approves](/docs/en/permission-modes#actions-no-mode-auto-approves); see [How permissions are evaluated](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated) for which of them reach the callback and what happens in `dontAsk` and `auto` mode.
54</Warning>
48> [!WARNING]
49>
50> **The callback never fires for auto-approved tools.** Any approval earlier in the [permission evaluation flow](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated), an allow rule or a mode like `acceptEdits` or `bypassPermissions`, resolves the call before `canUseTool` is consulted. If you list a tool bare in `allowed_tools`, a `canUseTool` check for that tool runs only when the [evaluation flow](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated) routes the call back to a prompt, such as an ask rule or `plan` mode. For logic that must apply to every tool call, use a [`PreToolUse` hook](/docs/en/agent-sdk/hooks), which executes before the rest of the flow and can allow, deny, or modify requests.
51>
52> An allow rule doesn't pre-approve the [actions no mode auto-approves](/docs/en/permission-modes#actions-no-mode-auto-approves); see [How permissions are evaluated](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated) for which of them reach the callback and what happens in `dontAsk` and `auto` mode.
5553
5654You can also use the [`PermissionRequest` hook](/docs/en/agent-sdk/hooks#available-hooks) to send external notifications (Slack, email, push) when Claude is waiting for approval.
5755
from line 80
8280
8381The following example asks Claude to create and delete a test file. When Claude attempts each operation, the callback prints the tool request to the terminal and prompts for y/n approval.
8482
85<CodeGroup>
86 ```python Python theme={null}
87 import asyncio
88
89 from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
90 from claude_agent_sdk.types import (
91 HookMatcher,
92 PermissionResultAllow,
93 PermissionResultDeny,
94 ToolPermissionContext,
95 )
96
97
98 async def can_use_tool(
99 tool_name: str, input_data: dict, context: ToolPermissionContext
100 ) -> PermissionResultAllow | PermissionResultDeny:
101 # Display the tool request
102 print(f"\nTool: {tool_name}")
103 if tool_name == "Bash":
104 print(f"Command: {input_data.get('command')}")
105 if input_data.get("description"):
106 print(f"Description: {input_data.get('description')}")
107 else:
108 print(f"Input: {input_data}")
109
110 # Get user approval
111 response = input("Allow this action? (y/n): ")
112
113 # Return allow or deny based on user's response
114 if response.lower() == "y":
115 # Allow: tool executes with the original (or modified) input
116 return PermissionResultAllow(updated_input=input_data)
117 else:
118 # Deny: tool doesn't execute, Claude sees the message
119 return PermissionResultDeny(message="User denied this action")
120
121
122 # Required workaround: dummy hook keeps the stream open for can_use_tool
123 async def dummy_hook(input_data, tool_use_id, context):
124 return {"continue_": True}
125
126
127 async def prompt_stream():
128 yield {
129 "type": "user",
130 "message": {
131 "role": "user",
132 "content": "Create a test file in /tmp and then delete it",
133 },
83```python Python theme={null}
84import asyncio
85
86from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
87from claude_agent_sdk.types import (
88 HookMatcher,
89 PermissionResultAllow,
90 PermissionResultDeny,
91 ToolPermissionContext,
92)
93
94
95async def can_use_tool(
96 tool_name: str, input_data: dict, context: ToolPermissionContext
97) -> PermissionResultAllow | PermissionResultDeny:
98 # Display the tool request
99 print(f"\nTool: {tool_name}")
100 if tool_name == "Bash":
101 print(f"Command: {input_data.get('command')}")
102 if input_data.get("description"):
103 print(f"Description: {input_data.get('description')}")
104 else:
105 print(f"Input: {input_data}")
106
107 # Get user approval
108 response = input("Allow this action? (y/n): ")
109
110 # Return allow or deny based on user's response
111 if response.lower() == "y":
112 # Allow: tool executes with the original (or modified) input
113 return PermissionResultAllow(updated_input=input_data)
114 else:
115 # Deny: tool doesn't execute, Claude sees the message
116 return PermissionResultDeny(message="User denied this action")
117
118
119# Required workaround: dummy hook keeps the stream open for can_use_tool
120async def dummy_hook(input_data, tool_use_id, context):
121 return {"continue_": True}
122
123
124async def prompt_stream():
125 yield {
126 "type": "user",
127 "message": {
128 "role": "user",
129 "content": "Create a test file in /tmp and then delete it",
130 },
131 }
132
133
134async def main():
135 async for message in query(
136 prompt=prompt_stream(),
137 options=ClaudeAgentOptions(
138 can_use_tool=can_use_tool,
139 hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
140 ),
141 ):
142 if isinstance(message, ResultMessage) and message.subtype == "success":
143 print(message.result)
144
145
146asyncio.run(main())
147```
148
149```typescript TypeScript theme={null}
150import { query } from "@anthropic-ai/claude-agent-sdk";
151import * as readline from "readline";
152
153// Helper to prompt user for input in the terminal
154function prompt(question: string): Promise<string> {
155 const rl = readline.createInterface({
156 input: process.stdin,
157 output: process.stdout
158 });
159 return new Promise((resolve) =>
160 rl.question(question, (answer) => {
161 rl.close();
162 resolve(answer);
163 })
164 );
165}
166
167for await (const message of query({
168 prompt: "Create a test file in /tmp and then delete it",
169 options: {
170 canUseTool: async (toolName, input) => {
171 // Display the tool request
172 console.log(`\nTool: ${toolName}`);
173 if (toolName === "Bash") {
174 console.log(`Command: ${input.command}`);
175 if (input.description) console.log(`Description: ${input.description}`);
176 } else {
177 console.log(`Input: ${JSON.stringify(input, null, 2)}`);
134178 }
135179
136
137 async def main():
138 async for message in query(
139 prompt=prompt_stream(),
140 options=ClaudeAgentOptions(
141 can_use_tool=can_use_tool,
142 hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
143 ),
144 ):
145 if isinstance(message, ResultMessage) and message.subtype == "success":
146 print(message.result)
147
148
149 asyncio.run(main())
150 ```
151
152 ```typescript TypeScript theme={null}
153 import { query } from "@anthropic-ai/claude-agent-sdk";
154 import * as readline from "readline";
155
156 // Helper to prompt user for input in the terminal
157 function prompt(question: string): Promise<string> {
158 const rl = readline.createInterface({
159 input: process.stdin,
160 output: process.stdout
161 });
162 return new Promise((resolve) =>
163 rl.question(question, (answer) => {
164 rl.close();
165 resolve(answer);
166 })
167 );
168 }
169
170 for await (const message of query({
171 prompt: "Create a test file in /tmp and then delete it",
172 options: {
173 canUseTool: async (toolName, input) => {
174 // Display the tool request
175 console.log(`\nTool: ${toolName}`);
176 if (toolName === "Bash") {
177 console.log(`Command: ${input.command}`);
178 if (input.description) console.log(`Description: ${input.description}`);
179 } else {
180 console.log(`Input: ${JSON.stringify(input, null, 2)}`);
181 }
182
183 // Get user approval
184 const response = await prompt("Allow this action? (y/n): ");
185
186 // Return allow or deny based on user's response
187 if (response.toLowerCase() === "y") {
188 // Allow: tool executes with the original (or modified) input
189 return { behavior: "allow", updatedInput: input };
190 } else {
191 // Deny: tool doesn't execute, Claude sees the message
192 return { behavior: "deny", message: "User denied this action" };
193 }
180 // Get user approval
181 const response = await prompt("Allow this action? (y/n): ");
182
183 // Return allow or deny based on user's response
184 if (response.toLowerCase() === "y") {
185 // Allow: tool executes with the original (or modified) input
186 return { behavior: "allow", updatedInput: input };
187 } else {
188 // Deny: tool doesn't execute, Claude sees the message
189 return { behavior: "deny", message: "User denied this action" };
194190 }
195191 }
196 })) {
197 if ("result" in message) console.log(message.result);
198 }
199 ```
200</CodeGroup>
192 }
193})) {
194 if ("result" in message) console.log(message.result);
195}
196```
201197
202198This example uses a `y/n` flow where any input other than `y` is treated as a denial. In practice, you might build a richer UI that lets users modify the request, provide feedback, or redirect Claude entirely. See [Respond to tool requests](#respond-to-tool-requests) for all the ways you can respond.
203199
from line 221
225221
226222The `ask_user` and `askUser` helpers in the following snippets stand in for your application's own prompt UI.
227223
228<Tabs>
229 <Tab title="Approve">
230 The user approves the action as-is. Pass through the `input` from your callback unchanged and the tool executes exactly as Claude requested.
231
232 <CodeGroup>
233 ```python Python theme={null}
234 async def can_use_tool(tool_name, input_data, context):
235 print(f"Claude wants to use {tool_name}")
236 approved = await ask_user("Allow this action?")
237
238 if approved:
239 return PermissionResultAllow(updated_input=input_data)
240 return PermissionResultDeny(message="User declined")
241 ```
242
243 ```typescript TypeScript theme={null}
244 canUseTool: async (toolName, input) => {
245 console.log(`Claude wants to use ${toolName}`);
246 const approved = await askUser("Allow this action?");
247
248 if (approved) {
249 return { behavior: "allow", updatedInput: input };
250 }
251 return { behavior: "deny", message: "User declined" };
252 };
253 ```
254 </CodeGroup>
255 </Tab>
256
257 <Tab title="Approve with changes">
258 The user approves but wants to modify the request first. You can change the input before the tool executes. Claude sees the result but isn't told you changed anything. Useful for sanitizing parameters, adding constraints, or scoping access.
259
260 <CodeGroup>
261 ```python Python theme={null}
262 async def can_use_tool(tool_name, input_data, context):
263 if tool_name == "Bash":
264 # User approved, but scope all commands to sandbox
265 sandboxed_input = {**input_data}
266 sandboxed_input["command"] = input_data["command"].replace(
267 "/tmp", "/tmp/sandbox"
268 )
269 return PermissionResultAllow(updated_input=sandboxed_input)
270 return PermissionResultAllow(updated_input=input_data)
271 ```
272
273 ```typescript TypeScript theme={null}
274 canUseTool: async (toolName, input) => {
275 if (toolName === "Bash") {
276 // User approved, but scope all commands to sandbox
277 const sandboxedInput = {
278 ...input,
279 command: input.command.replace("/tmp", "/tmp/sandbox")
280 };
281 return { behavior: "allow", updatedInput: sandboxedInput };
282 }
283 return { behavior: "allow", updatedInput: input };
284 };
285 ```
286 </CodeGroup>
287 </Tab>
288
289 <Tab title="Approve and remember">
290 The user approves and doesn't want to be asked again for this kind of call. The third callback argument carries `suggestions`, an array of ready-made [`PermissionUpdate`](/docs/en/agent-sdk/typescript#permissionupdate) entries. Echo one back in `updatedPermissions` to apply it. A suggestion with the `localSettings` destination writes the rule to `.claude/settings.local.json` so future sessions skip the prompt for matching calls.
291
292 In TypeScript, skip the always-allow choice for a request whose options carry [`suppressAlwaysAllowRule: true`](/docs/en/agent-sdk/typescript#canusetool). The hint requires Agent SDK v0.3.268 or later, and the Python `context` doesn't carry it.
293
294 The Python example requires `claude-agent-sdk` 0.1.80 or later.
295
296 <CodeGroup>
297 ```python Python theme={null}
298 async def can_use_tool(tool_name, input_data, context):
299 choice = await ask_user(f"Allow {tool_name}?", ["once", "always", "no"])
300
301 if choice == "always":
302 persist = [
303 s for s in context.suggestions if s.destination == "localSettings"
304 ]
305 return PermissionResultAllow(
306 updated_input=input_data, updated_permissions=persist
307 )
308 if choice == "once":
309 return PermissionResultAllow(updated_input=input_data)
310 return PermissionResultDeny(message="User declined")
311 ```
312
313 ```typescript TypeScript theme={null}
314 canUseTool: async (toolName, input, { suggestions = [] }) => {
315 const choice = await askUser(`Allow ${toolName}?`, ["once", "always", "no"]);
316
317 if (choice === "always") {
318 const persist = suggestions.filter(
319 (s) => s.destination === "localSettings"
320 );
321 return {
322 behavior: "allow",
323 updatedInput: input,
324 updatedPermissions: persist
325 };
326 }
327 if (choice === "once") {
328 return { behavior: "allow", updatedInput: input };
329 }
330 return { behavior: "deny", message: "User declined" };
331 };
332 ```
333 </CodeGroup>
334 </Tab>
335
336 <Tab title="Reject">
337 The user doesn't want this action to happen. Block the tool and provide a message explaining why. Claude sees this message and may try a different approach.
338
339 <CodeGroup>
340 ```python Python theme={null}
341 async def can_use_tool(tool_name, input_data, context):
342 approved = await ask_user(f"Allow {tool_name}?")
343
344 if not approved:
345 return PermissionResultDeny(message="User rejected this action")
346 return PermissionResultAllow(updated_input=input_data)
347 ```
348
349 ```typescript TypeScript theme={null}
350 canUseTool: async (toolName, input) => {
351 const approved = await askUser(`Allow ${toolName}?`);
352
353 if (!approved) {
354 return {
355 behavior: "deny",
356 message: "User rejected this action"
357 };
358 }
359 return { behavior: "allow", updatedInput: input };
360 };
361 ```
362 </CodeGroup>
363 </Tab>
364
365 <Tab title="Suggest alternative">
366 The user doesn't want this specific action, but has a different idea. Block the tool and include guidance in your message. Claude will read this and decide how to proceed based on your feedback.
367
368 <CodeGroup>
369 ```python Python theme={null}
370 async def can_use_tool(tool_name, input_data, context):
371 if tool_name == "Bash" and "rm" in input_data.get("command", ""):
372 # User doesn't want to delete, suggest archiving instead
373 return PermissionResultDeny(
374 message="User doesn't want to delete files. They asked if you could compress them into an archive instead."
375 )
376 return PermissionResultAllow(updated_input=input_data)
377 ```
378
379 ```typescript TypeScript theme={null}
380 canUseTool: async (toolName, input) => {
381 if (toolName === "Bash" && input.command.includes("rm")) {
382 // User doesn't want to delete, suggest archiving instead
383 return {
384 behavior: "deny",
385 message:
386 "User doesn't want to delete files. They asked if you could compress them into an archive instead."
387 };
388 }
389 return { behavior: "allow", updatedInput: input };
390 };
391 ```
392 </CodeGroup>
393 </Tab>
394
395 <Tab title="Redirect entirely">
396 For a complete change of direction (not just a nudge), use [streaming input](/docs/en/agent-sdk/streaming-vs-single-mode) to send Claude a new instruction directly. This bypasses the current tool request and gives Claude entirely new instructions to follow.
397 </Tab>
398</Tabs>
224**Approve**
225
226The user approves the action as-is. Pass through the `input` from your callback unchanged and the tool executes exactly as Claude requested.
227
228```python Python theme={null}
229async def can_use_tool(tool_name, input_data, context):
230 print(f"Claude wants to use {tool_name}")
231 approved = await ask_user("Allow this action?")
232
233 if approved:
234 return PermissionResultAllow(updated_input=input_data)
235 return PermissionResultDeny(message="User declined")
236```
237
238```typescript TypeScript theme={null}
239canUseTool: async (toolName, input) => {
240 console.log(`Claude wants to use ${toolName}`);
241 const approved = await askUser("Allow this action?");
242
243 if (approved) {
244 return { behavior: "allow", updatedInput: input };
245 }
246 return { behavior: "deny", message: "User declined" };
247};
248```
249
250**Approve with changes**
251
252The user approves but wants to modify the request first. You can change the input before the tool executes. Claude sees the result but isn't told you changed anything. Useful for sanitizing parameters, adding constraints, or scoping access.
253
254```python Python theme={null}
255async def can_use_tool(tool_name, input_data, context):
256 if tool_name == "Bash":
257 # User approved, but scope all commands to sandbox
258 sandboxed_input = {**input_data}
259 sandboxed_input["command"] = input_data["command"].replace(
260 "/tmp", "/tmp/sandbox"
261 )
262 return PermissionResultAllow(updated_input=sandboxed_input)
263 return PermissionResultAllow(updated_input=input_data)
264```
265
266```typescript TypeScript theme={null}
267canUseTool: async (toolName, input) => {
268 if (toolName === "Bash") {
269 // User approved, but scope all commands to sandbox
270 const sandboxedInput = {
271 ...input,
272 command: input.command.replace("/tmp", "/tmp/sandbox")
273 };
274 return { behavior: "allow", updatedInput: sandboxedInput };
275 }
276 return { behavior: "allow", updatedInput: input };
277};
278```
279
280**Approve and remember**
281
282The user approves and doesn't want to be asked again for this kind of call. The third callback argument carries `suggestions`, an array of ready-made [`PermissionUpdate`](/docs/en/agent-sdk/typescript#permissionupdate) entries. Echo one back in `updatedPermissions` to apply it. A suggestion with the `localSettings` destination writes the rule to `.claude/settings.local.json` so future sessions skip the prompt for matching calls.
283
284In TypeScript, skip the always-allow choice for a request whose options carry [`suppressAlwaysAllowRule: true`](/docs/en/agent-sdk/typescript#canusetool). The hint requires Agent SDK v0.3.268 or later, and the Python `context` doesn't carry it.
285
286The Python example requires `claude-agent-sdk` 0.1.80 or later.
287
288```python Python theme={null}
289async def can_use_tool(tool_name, input_data, context):
290 choice = await ask_user(f"Allow {tool_name}?", ["once", "always", "no"])
291
292 if choice == "always":
293 persist = [
294 s for s in context.suggestions if s.destination == "localSettings"
295 ]
296 return PermissionResultAllow(
297 updated_input=input_data, updated_permissions=persist
298 )
299 if choice == "once":
300 return PermissionResultAllow(updated_input=input_data)
301 return PermissionResultDeny(message="User declined")
302```
303
304```typescript TypeScript theme={null}
305canUseTool: async (toolName, input, { suggestions = [] }) => {
306 const choice = await askUser(`Allow ${toolName}?`, ["once", "always", "no"]);
307
308 if (choice === "always") {
309 const persist = suggestions.filter(
310 (s) => s.destination === "localSettings"
311 );
312 return {
313 behavior: "allow",
314 updatedInput: input,
315 updatedPermissions: persist
316 };
317 }
318 if (choice === "once") {
319 return { behavior: "allow", updatedInput: input };
320 }
321 return { behavior: "deny", message: "User declined" };
322};
323```
324
325**Reject**
326
327The user doesn't want this action to happen. Block the tool and provide a message explaining why. Claude sees this message and may try a different approach.
328
329```python Python theme={null}
330async def can_use_tool(tool_name, input_data, context):
331 approved = await ask_user(f"Allow {tool_name}?")
332
333 if not approved:
334 return PermissionResultDeny(message="User rejected this action")
335 return PermissionResultAllow(updated_input=input_data)
336```
337
338```typescript TypeScript theme={null}
339canUseTool: async (toolName, input) => {
340 const approved = await askUser(`Allow ${toolName}?`);
341
342 if (!approved) {
343 return {
344 behavior: "deny",
345 message: "User rejected this action"
346 };
347 }
348 return { behavior: "allow", updatedInput: input };
349};
350```
351
352**Suggest alternative**
353
354The user doesn't want this specific action, but has a different idea. Block the tool and include guidance in your message. Claude will read this and decide how to proceed based on your feedback.
355
356```python Python theme={null}
357async def can_use_tool(tool_name, input_data, context):
358 if tool_name == "Bash" and "rm" in input_data.get("command", ""):
359 # User doesn't want to delete, suggest archiving instead
360 return PermissionResultDeny(
361 message="User doesn't want to delete files. They asked if you could compress them into an archive instead."
362 )
363 return PermissionResultAllow(updated_input=input_data)
364```
365
366```typescript TypeScript theme={null}
367canUseTool: async (toolName, input) => {
368 if (toolName === "Bash" && input.command.includes("rm")) {
369 // User doesn't want to delete, suggest archiving instead
370 return {
371 behavior: "deny",
372 message:
373 "User doesn't want to delete files. They asked if you could compress them into an archive instead."
374 };
375 }
376 return { behavior: "allow", updatedInput: input };
377};
378```
379
380**Redirect entirely**
381
382For a complete change of direction (not just a nudge), use [streaming input](/docs/en/agent-sdk/streaming-vs-single-mode) to send Claude a new instruction directly. This bypasses the current tool request and gives Claude entirely new instructions to follow.
399383
400384## Handle clarifying questions
401385
402386When 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.
403387
404<Tip>
405 Clarifying questions are especially common in [`plan` mode](/docs/en/agent-sdk/permissions#plan-mode-plan), 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.
406</Tip>
388> [!TIP]
389>
390> Clarifying questions are especially common in [`plan` mode](/docs/en/agent-sdk/permissions#plan-mode-plan), 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.
407391
408392The following steps show how to handle clarifying questions:
409393
410<Steps>
411 <Step title="Pass a canUseTool callback">
412 Pass a `canUseTool` callback in your query options. By default, `AskUserQuestion` is available. If you specify a `tools` array to restrict Claude's capabilities (for example, a read-only agent with only `Read`, `Glob`, and `Grep`), include `AskUserQuestion` in that array. Otherwise, Claude won't be able to ask clarifying questions:
413
414 <CodeGroup>
415 ```python Python theme={null}
416 async for message in query(
417 prompt="Analyze this codebase",
418 options=ClaudeAgentOptions(
419 # Include AskUserQuestion in your tools list
420 tools=["Read", "Glob", "Grep", "AskUserQuestion"],
421 can_use_tool=can_use_tool,
422 ),
423 ):
424 print(message)
425 ```
426
427 ```typescript TypeScript theme={null}
428 for await (const message of query({
429 prompt: "Analyze this codebase",
430 options: {
431 // Include AskUserQuestion in your tools list
432 tools: ["Read", "Glob", "Grep", "AskUserQuestion"],
433 canUseTool: async (toolName, input) => {
434 // Placeholder that approves every call. The Detect AskUserQuestion step replaces it.
435 return { behavior: "allow", updatedInput: input };
436 }
437 }
438 })) {
439 console.log(message);
440 }
441 ```
442 </CodeGroup>
443 </Step>
444
445 <Step title="Detect AskUserQuestion">
446 In your callback, check if `toolName` equals `AskUserQuestion` to handle it differently from other tools:
447
448 <CodeGroup>
449 ```python Python theme={null}
450 async def can_use_tool(tool_name: str, input_data: dict, context):
451 if tool_name == "AskUserQuestion":
452 # Your implementation to collect answers from the user
453 return await handle_clarifying_questions(input_data)
454 # Handle other tools normally
455 return await prompt_for_approval(tool_name, input_data)
456 ```
457
458 ```typescript TypeScript theme={null}
459 canUseTool: async (toolName, input) => {
460 if (toolName === "AskUserQuestion") {
461 // Your implementation to collect answers from the user
462 return handleClarifyingQuestions(input);
463 }
464 // Handle other tools normally
465 return promptForApproval(toolName, input);
466 };
467 ```
468 </CodeGroup>
469 </Step>
470
471 <Step title="Parse the question input">
472 The input contains Claude's questions in a `questions` array. Each question has a `question` (the text to display), `options` (the choices), and `multiSelect` (whether multiple selections are allowed):
473
474 ```json theme={null}
394**1. Pass a canUseTool callback**
395
396Pass a `canUseTool` callback in your query options. By default, `AskUserQuestion` is available. If you specify a `tools` array to restrict Claude's capabilities (for example, a read-only agent with only `Read`, `Glob`, and `Grep`), include `AskUserQuestion` in that array. Otherwise, Claude won't be able to ask clarifying questions:
397
398```python Python theme={null}
399async for message in query(
400 prompt="Analyze this codebase",
401 options=ClaudeAgentOptions(
402 # Include AskUserQuestion in your tools list
403 tools=["Read", "Glob", "Grep", "AskUserQuestion"],
404 can_use_tool=can_use_tool,
405 ),
406):
407 print(message)
408```
409
410```typescript TypeScript theme={null}
411for await (const message of query({
412 prompt: "Analyze this codebase",
413 options: {
414 // Include AskUserQuestion in your tools list
415 tools: ["Read", "Glob", "Grep", "AskUserQuestion"],
416 canUseTool: async (toolName, input) => {
417 // Placeholder that approves every call. The Detect AskUserQuestion step replaces it.
418 return { behavior: "allow", updatedInput: input };
419 }
420 }
421})) {
422 console.log(message);
423}
424```
425
426**2. Detect AskUserQuestion**
427
428In your callback, check if `toolName` equals `AskUserQuestion` to handle it differently from other tools:
429
430```python Python theme={null}
431async def can_use_tool(tool_name: str, input_data: dict, context):
432 if tool_name == "AskUserQuestion":
433 # Your implementation to collect answers from the user
434 return await handle_clarifying_questions(input_data)
435 # Handle other tools normally
436 return await prompt_for_approval(tool_name, input_data)
437```
438
439```typescript TypeScript theme={null}
440canUseTool: async (toolName, input) => {
441 if (toolName === "AskUserQuestion") {
442 // Your implementation to collect answers from the user
443 return handleClarifyingQuestions(input);
444 }
445 // Handle other tools normally
446 return promptForApproval(toolName, input);
447};
448```
449
450**3. Parse the question input**
451
452The input contains Claude's questions in a `questions` array. Each question has a `question` (the text to display), `options` (the choices), and `multiSelect` (whether multiple selections are allowed):
453
454```json theme={null}
455{
456 "questions": [
475457 {
476 "questions": [
477 {
478 "question": "How should I format the output?",
479 "header": "Format",
480 "options": [
481 { "label": "Summary", "description": "Brief overview" },
482 { "label": "Detailed", "description": "Full explanation" }
483 ],
484 "multiSelect": false
458 "question": "How should I format the output?",
459 "header": "Format",
460 "options": [
461 { "label": "Summary", "description": "Brief overview" },
462 { "label": "Detailed", "description": "Full explanation" }
463 ],
464 "multiSelect": false
465 },
466 {
467 "question": "Which sections should I include?",
468 "header": "Sections",
469 "options": [
470 { "label": "Introduction", "description": "Opening context" },
471 { "label": "Conclusion", "description": "Final summary" }
472 ],
473 "multiSelect": true
474 }
475 ]
476}
477```
478
479See [Question format](#question-format) for full field descriptions.
480
481**4. Collect answers from the user**
482
483Present the questions to the user and collect their selections. How you do this depends on your application: a terminal prompt, a web form, a mobile dialog, etc.
484
485**5. Return answers to Claude**
486
487Build the `answers` object as a record where each key is the `question` text and each value is the selected option's `label`:
488
489| From the question object | Use as |
490| - | - |
491| `question` field (for example, `"How should I format the output?"`) | Key |
492| Selected option's `label` field (for example, `"Summary"`) | Value |
493
494For multi-select questions, pass an array of labels or join them with `", "`. If you [support free-text input](#support-free-text-input), use the user's custom text as the value.
495
496```python Python theme={null}
497return PermissionResultAllow(
498 updated_input={
499 "questions": input_data.get("questions", []),
500 "answers": {
501 "How should I format the output?": "Summary",
502 "Which sections should I include?": ["Introduction", "Conclusion"],
485503 },
486 {
487 "question": "Which sections should I include?",
488 "header": "Sections",
489 "options": [
490 { "label": "Introduction", "description": "Opening context" },
491 { "label": "Conclusion", "description": "Final summary" }
492 ],
493 "multiSelect": true
494 }
495 ]
496 }
497 ```
498
499 See [Question format](#question-format) for full field descriptions.
500 </Step>
501
502 <Step title="Collect answers from the user">
503 Present the questions to the user and collect their selections. How you do this depends on your application: a terminal prompt, a web form, a mobile dialog, etc.
504 </Step>
505
506 <Step title="Return answers to Claude">
507 Build the `answers` object as a record where each key is the `question` text and each value is the selected option's `label`:
508
509 | From the question object | Use as |
510 | - | - |
511 | `question` field (for example, `"How should I format the output?"`) | Key |
512 | Selected option's `label` field (for example, `"Summary"`) | Value |
513
514 For multi-select questions, pass an array of labels or join them with `", "`. If you [support free-text input](#support-free-text-input), use the user's custom text as the value.
515
516 <CodeGroup>
517 ```python Python theme={null}
518 return PermissionResultAllow(
519 updated_input={
520 "questions": input_data.get("questions", []),
521 "answers": {
522 "How should I format the output?": "Summary",
523 "Which sections should I include?": ["Introduction", "Conclusion"],
524 },
525 }
526 )
527 ```
528
529 ```typescript TypeScript theme={null}
530 return {
531 behavior: "allow",
532 updatedInput: {
533 questions: input.questions,
534 answers: {
535 "How should I format the output?": "Summary",
536 "Which sections should I include?": "Introduction, Conclusion"
537 }
538 }
539 };
540 ```
541 </CodeGroup>
542 </Step>
543</Steps>
504 }
505)
506```
507
508```typescript TypeScript theme={null}
509return {
510 behavior: "allow",
511 updatedInput: {
512 questions: input.questions,
513 answers: {
514 "How should I format the output?": "Summary",
515 "Which sections should I include?": "Introduction, Conclusion"
516 }
517 }
518};
519```
544520
545521### Question format
546522
from line 635
659635
660636Save the TypeScript version as `ask.ts` and run it with `npx tsx ask.ts`, or save the Python version as `ask.py` and run it with `python ask.py`.
661637
662<CodeGroup>
663 ```python Python theme={null}
664 import asyncio
665
666 from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
667 from claude_agent_sdk.types import HookMatcher, PermissionResultAllow
668
669
670 def parse_response(response: str, options: list) -> str:
671 """Parse user input as option number(s) or free text."""
672 try:
673 indices = [int(s.strip()) - 1 for s in response.split(",")]
674 labels = [options[i]["label"] for i in indices if 0 <= i < len(options)]
675 return ", ".join(labels) if labels else response
676 except ValueError:
677 return response
678
679
680 async def handle_ask_user_question(input_data: dict) -> PermissionResultAllow:
681 """Display Claude's questions and collect user answers."""
682 answers = {}
683
684 for q in input_data.get("questions", []):
685 print(f"\n{q['header']}: {q['question']}")
686
687 options = q["options"]
688 for i, opt in enumerate(options):
689 print(f" {i + 1}. {opt['label']} - {opt['description']}")
690 if q.get("multiSelect"):
691 print(" (Enter numbers separated by commas, or type your own answer)")
692 else:
693 print(" (Enter a number, or type your own answer)")
694
695 response = input("Your choice: ").strip()
696 answers[q["question"]] = parse_response(response, options)
697
698 return PermissionResultAllow(
699 updated_input={
700 "questions": input_data.get("questions", []),
701 "answers": answers,
702 }
703 )
704
705
706 async def can_use_tool(
707 tool_name: str, input_data: dict, context
708 ) -> PermissionResultAllow:
709 # Route AskUserQuestion to our question handler
710 if tool_name == "AskUserQuestion":
711 return await handle_ask_user_question(input_data)
712 # Auto-approve other tools for this example
713 return PermissionResultAllow(updated_input=input_data)
714
715
716 async def prompt_stream():
717 yield {
718 "type": "user",
719 "message": {
720 "role": "user",
721 "content": "Help me decide on the tech stack for a new mobile app",
722 },
638```python Python theme={null}
639import asyncio
640
641from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
642from claude_agent_sdk.types import HookMatcher, PermissionResultAllow
643
644
645def parse_response(response: str, options: list) -> str:
646 """Parse user input as option number(s) or free text."""
647 try:
648 indices = [int(s.strip()) - 1 for s in response.split(",")]
649 labels = [options[i]["label"] for i in indices if 0 <= i < len(options)]
650 return ", ".join(labels) if labels else response
651 except ValueError:
652 return response
653
654
655async def handle_ask_user_question(input_data: dict) -> PermissionResultAllow:
656 """Display Claude's questions and collect user answers."""
657 answers = {}
658
659 for q in input_data.get("questions", []):
660 print(f"\n{q['header']}: {q['question']}")
661
662 options = q["options"]
663 for i, opt in enumerate(options):
664 print(f" {i + 1}. {opt['label']} - {opt['description']}")
665 if q.get("multiSelect"):
666 print(" (Enter numbers separated by commas, or type your own answer)")
667 else:
668 print(" (Enter a number, or type your own answer)")
669
670 response = input("Your choice: ").strip()
671 answers[q["question"]] = parse_response(response, options)
672
673 return PermissionResultAllow(
674 updated_input={
675 "questions": input_data.get("questions", []),
676 "answers": answers,
677 }
678 )
679
680
681async def can_use_tool(
682 tool_name: str, input_data: dict, context
683) -> PermissionResultAllow:
684 # Route AskUserQuestion to our question handler
685 if tool_name == "AskUserQuestion":
686 return await handle_ask_user_question(input_data)
687 # Auto-approve other tools for this example
688 return PermissionResultAllow(updated_input=input_data)
689
690
691async def prompt_stream():
692 yield {
693 "type": "user",
694 "message": {
695 "role": "user",
696 "content": "Help me decide on the tech stack for a new mobile app",
697 },
698 }
699
700
701# Required workaround: dummy hook keeps the stream open for can_use_tool
702async def dummy_hook(input_data, tool_use_id, context):
703 return {"continue_": True}
704
705
706async def main():
707 async for message in query(
708 prompt=prompt_stream(),
709 options=ClaudeAgentOptions(
710 can_use_tool=can_use_tool,
711 hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
712 ),
713 ):
714 if isinstance(message, ResultMessage) and message.subtype == "success":
715 print(message.result)
716
717
718asyncio.run(main())
719```
720
721```typescript TypeScript theme={null}
722import { query } from "@anthropic-ai/claude-agent-sdk";
723import type { PermissionResult } from "@anthropic-ai/claude-agent-sdk";
724import * as readline from "readline/promises";
725
726// Helper to prompt user for input in the terminal
727async function prompt(question: string): Promise<string> {
728 const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
729 const answer = await rl.question(question);
730 rl.close();
731 return answer;
732}
733
734// Parse user input as option number(s) or free text
735function parseResponse(response: string, options: any[]): string {
736 const indices = response.split(",").map((s) => parseInt(s.trim()) - 1);
737 const labels = indices
738 .filter((i) => !isNaN(i) && i >= 0 && i < options.length)
739 .map((i) => options[i].label);
740 return labels.length > 0 ? labels.join(", ") : response;
741}
742
743// Display Claude's questions and collect user answers
744async function handleAskUserQuestion(input: any): Promise<PermissionResult> {
745 const answers: Record<string, string> = {};
746
747 for (const q of input.questions) {
748 console.log(`\n${q.header}: ${q.question}`);
749
750 const options = q.options;
751 options.forEach((opt: any, i: number) => {
752 console.log(` ${i + 1}. ${opt.label} - ${opt.description}`);
753 });
754 if (q.multiSelect) {
755 console.log(" (Enter numbers separated by commas, or type your own answer)");
756 } else {
757 console.log(" (Enter a number, or type your own answer)");
758 }
759
760 const response = (await prompt("Your choice: ")).trim();
761 answers[q.question] = parseResponse(response, options);
762 }
763
764 // Return the answers to Claude (must include original questions)
765 return {
766 behavior: "allow",
767 updatedInput: { questions: input.questions, answers }
768 };
769}
770
771async function main() {
772 for await (const message of query({
773 prompt: "Help me decide on the tech stack for a new mobile app",
774 options: {
775 canUseTool: async (toolName, input) => {
776 // Route AskUserQuestion to our question handler
777 if (toolName === "AskUserQuestion") {
778 return handleAskUserQuestion(input);
779 }
780 // Auto-approve other tools for this example
781 return { behavior: "allow", updatedInput: input };
723782 }
724
725
726 # Required workaround: dummy hook keeps the stream open for can_use_tool
727 async def dummy_hook(input_data, tool_use_id, context):
728 return {"continue_": True}
729
730
731 async def main():
732 async for message in query(
733 prompt=prompt_stream(),
734 options=ClaudeAgentOptions(
735 can_use_tool=can_use_tool,
736 hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
737 ),
738 ):
739 if isinstance(message, ResultMessage) and message.subtype == "success":
740 print(message.result)
741
742
743 asyncio.run(main())
744 ```
745
746 ```typescript TypeScript theme={null}
747 import { query } from "@anthropic-ai/claude-agent-sdk";
748 import type { PermissionResult } from "@anthropic-ai/claude-agent-sdk";
749 import * as readline from "readline/promises";
750
751 // Helper to prompt user for input in the terminal
752 async function prompt(question: string): Promise<string> {
753 const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
754 const answer = await rl.question(question);
755 rl.close();
756 return answer;
757 }
758
759 // Parse user input as option number(s) or free text
760 function parseResponse(response: string, options: any[]): string {
761 const indices = response.split(",").map((s) => parseInt(s.trim()) - 1);
762 const labels = indices
763 .filter((i) => !isNaN(i) && i >= 0 && i < options.length)
764 .map((i) => options[i].label);
765 return labels.length > 0 ? labels.join(", ") : response;
766 }
767
768 // Display Claude's questions and collect user answers
769 async function handleAskUserQuestion(input: any): Promise<PermissionResult> {
770 const answers: Record<string, string> = {};
771
772 for (const q of input.questions) {
773 console.log(`\n${q.header}: ${q.question}`);
774
775 const options = q.options;
776 options.forEach((opt: any, i: number) => {
777 console.log(` ${i + 1}. ${opt.label} - ${opt.description}`);
778 });
779 if (q.multiSelect) {
780 console.log(" (Enter numbers separated by commas, or type your own answer)");
781 } else {
782 console.log(" (Enter a number, or type your own answer)");
783 }
784
785 const response = (await prompt("Your choice: ")).trim();
786 answers[q.question] = parseResponse(response, options);
787 }
788
789 // Return the answers to Claude (must include original questions)
790 return {
791 behavior: "allow",
792 updatedInput: { questions: input.questions, answers }
793 };
794 }
795
796 async function main() {
797 for await (const message of query({
798 prompt: "Help me decide on the tech stack for a new mobile app",
799 options: {
800 canUseTool: async (toolName, input) => {
801 // Route AskUserQuestion to our question handler
802 if (toolName === "AskUserQuestion") {
803 return handleAskUserQuestion(input);
804 }
805 // Auto-approve other tools for this example
806 return { behavior: "allow", updatedInput: input };
807 }
808 }
809 })) {
810 if ("result" in message) console.log(message.result);
811 }
812 }
813
814 main();
815 ```
816</CodeGroup>
783 }
784 })) {
785 if ("result" in message) console.log(message.result);
786 }
787}
788
789main();
790```
817791
818792## Limitations
819793
820794
No line in this hunk matches that.