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 30 Oct 2026 18:07 UTC.
Upstream edited
Recorded here
Lines+159added
Lines−159removed
From line
221
where the diff opens
First seen
14 Aug 2026
this site's first read of the page
Recorded edits16to this page, all time
The whole hunk
from line 221, old and new numbered
/
from line 221
221221
222222The `ask_user` and `askUser` helpers in the following snippets stand in for your application's own prompt UI.
223223
224**Approve**
224> [tab] Approve
225>
226> The 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}
229> async 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}
239> canUseTool: 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> ```
225249
226The user approves the action as-is. Pass through the `input` from your callback unchanged and the tool executes exactly as Claude requested.
250> [tab] Approve with changes
251>
252> 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.
253>
254> ```python Python theme={null}
255> async 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}
267> canUseTool: 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> ```
227279
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?")
280> [tab] Approve and remember
281>
282> 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`](https://code.claude.com/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>
284> In TypeScript, skip the always-allow choice for a request whose options carry [`suppressAlwaysAllowRule: true`](https://code.claude.com/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>
286> The Python example requires `claude-agent-sdk` 0.1.80 or later.
287>
288> ```python Python theme={null}
289> async 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}
305> canUseTool: 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> ```
232324
233 if approved:
234 return PermissionResultAllow(updated_input=input_data)
235 return PermissionResultDeny(message="User declined")
236```
325> [tab] Reject
326>
327> 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.
328>
329> ```python Python theme={null}
330> async 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}
339> canUseTool: 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> ```
237351
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?");
352> [tab] Suggest alternative
353>
354> 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.
355>
356> ```python Python theme={null}
357> async 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}
367> canUseTool: 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> ```
242379
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`](https://code.claude.com/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`](https://code.claude.com/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](https://code.claude.com/docs/en/agent-sdk/streaming-vs-single-mode.md) to send Claude a new instruction directly. This bypasses the current tool request and gives Claude entirely new instructions to follow.
380> [tab] Redirect entirely
381>
382> For a complete change of direction (not just a nudge), use [streaming input](https://code.claude.com/docs/en/agent-sdk/streaming-vs-single-mode.md) to send Claude a new instruction directly. This bypasses the current tool request and gives Claude entirely new instructions to follow.
383383
384384## Handle clarifying questions
385385
No line in this hunk matches that.