Follow Discord
Sweep 22 Oct 2026 · 04:32Z Build v2.1.308 527 read Stable v2.1.308 Latest v2.1.308 Next v2.1.308 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · claude-code

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
/
lines

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 
Feedback