What you will be able to do
- Implement a canUseTool callback that sends tool calls to a human and returns allow or deny
- Choose among approve, modify, remember, reject and redirect responses at a checkpoint
- Configure Managed Agents permission policies so risky tools wait for confirmation
- Walk through the always_ask confirmation event flow and explain what a client can and cannot override
1.canUseTool: sending tool calls to a person
When you build your own agent with the Agent SDK, there is no built-in prompt UI, so you supply the human checkpoint yourself. The SDK calls your canUseTool callback (can_use_tool in Python) when Claude wants a tool that isn't "auto-approved by a permission rule or permission mode." The same callback receives clarifying questions: to handle those separately, "Check if tool_name == \"AskUserQuestion\" to handle it differently." Your callback shows the request to a person and returns their decision. With an allow, the tool runs. With a deny, the tool doesn't run and Claude sees your message.
Checkpoint 1 of 6· Fill the gap
Which parameter name lets the callback pass the (possibly edited) input back when the human approves?
async def can_use_tool(tool_name, input_data, context):
print(f"Claude wants to use {tool_name}")
approved = await ask_user("Allow this action?")
if approved:
return PermissionResultAllow( ? =input_data)
return PermissionResultDeny(message="User declined")An allow carries updated_input. Passing the original input approves as requested; passing an edited copy is how you approve with changes.
Source: code.claude.comA plain yes or no wastes much of what the reviewer knows. The SDK documentation lists six ways to respond. Several of them let the human correct the agent instead of just stopping it. Approving with changes means returning an allow whose updated_input has been edited, for example to sanitize a path. Approving and remembering turns one decision into a permission rule, which cuts down on repeat prompts.
| Response | What the human does |
|---|---|
| Approve | Let the tool execute as Claude requested |
| Approve with changes | Modify the input before execution (sanitize paths, add constraints) |
| Approve and remember | Echo a suggested permission rule back so matching calls skip the prompt next time |
| Reject | Block the tool and tell Claude why |
| Suggest alternative | Block but guide Claude toward what the user wants instead |
| Redirect entirely | Use streaming input to send Claude a completely new instruction |
The last two patterns differ from a plain reject in what they aim for. A reject says no and gives the reason. Suggest alternative is also a block, but the message steers Claude toward what the user wants instead, and Claude sees that message when the tool is denied. Redirect entirely goes further: it doesn't answer the pending request but uses streaming input to send Claude a new instruction. That is why the SDK's approval example feeds its prompt through a streaming generator. The documentation names these two patterns without a dedicated code example.
The callback only fires for calls that no rule or mode has already settled. The permission docs say that "Ask rules prompt for confirmation whenever Claude Code tries to use the specified tool," so an ask rule keeps a tool in front of a human. A mode can also remove the prompt: dontAsk "Auto-denies every call that would otherwise prompt," which suits locked-down CI where nobody is there to answer. Plan mode is for exploring, and the docs say Claude Code "blocks edits until you approve a plan." That approval is of the plan, not of each individual edit.
Checkpoint 2 of 6· Exam question
A DevOps team runs a Claude agent unattended inside a CI pipeline to triage failing builds. No human is available to respond to interactive approval prompts during the run, so any tool call that isn't already pre-approved must be denied outright rather than left waiting on a prompt. Which permission mode configuration achieves this?
Correct answer: A — Set permissionMode to "dontAsk" and list the CI-safe tools in allowedTools, so listed calls run automatically and everything else is denied without ever reaching canUseTool.
- A. Correct. dontAsk converts any call not pre-approved by allowedTools, settings.json allow rules, or a hook into an outright denial and never calls canUseTool, which is exactly the unattended behavior the pipeline needs.
- B. Incorrect. Omitting a callback under default mode is not documented to resolve as an automatic denial; default mode expects canUseTool to be present to answer unmatched calls.
- C. Incorrect. acceptEdits only auto-approves file edits and specific filesystem commands; anything else still falls through to canUseTool rather than being denied, which would hang an unattended run.
- D. Incorrect. plan mode still runs read-only tools normally and routes file-edit or shell-write tools to canUseTool, so approval prompts are not eliminated and CI triage needing real tool calls would stall.
2.Hooks as a second checkpoint layer
Hooks let you step in at fixed points in execution. The SDK hooks documentation lists human checkpoints as one use: "Require human approval for sensitive actions like database writes or API calls." When an event such as PreToolUse fires, the SDK collects the registered hooks, and matchers decide which ones run. The callbacks then execute and return a decision. A hook can deny a call in code before anyone sees it, for example to protect .env files, so the human only reviews calls that pass the automated rules. This keeps reviewers focused on the cases that need judgement.
Checkpoint 3 of 6· Check yourself
Which of these hook uses does the SDK documentation list as the way to put a person in the loop?
Hooks have several documented uses; requiring human approval for sensitive actions is the one that creates a human checkpoint. Logging and input transformation support oversight but don't ask a person.
“Require human approval for sensitive actions like database writes or API calls”Source: code.claude.com
Hooks and canUseTool can work together. In the SDK's Python approval example, a PreToolUse hook is registered next to can_use_tool, and the source comments it as a "Required workaround: dummy hook keeps the stream open for can_use_tool." The hook there makes no decision. It only keeps the stream open so the callback can reach the human.
3.Managed Agents: always_allow, always_ask and auto
In Managed Agents, the server runs the tools, and permission policies control whether a call runs, waits for you or gets evaluated. always_ask is the explicit human checkpoint: the session pauses until you approve. The defaults differ by toolset: "the agent toolset defaults to always_allow, and MCP toolsets default to always_ask." The MCP default exists so "new tools added to an MCP server do not execute in your application without approval." Custom tools fall outside this system, because your application runs them and you control them. You can override the default for a single tool, which is how you put one risky tool behind a checkpoint:
---
name: Coding Assistant
model: claude-opus-5-5
tools:
- type: agent_toolset_20260401
default_config:
permission_policy:
type: always_allow
configs:
- name: bash
permission_policy:
type: always_ask
---Under auto, the server evaluates every call and does one of three things: it runs the call, denies it as high-risk, or, when it "reaches no determination, the session pauses as it does under always_ask." So the human sees only the uncertain cases. There is one catch to know. The server treats user.message content as your intent, and that intent can get a call allowed. If you relay untrusted end-user text through user.message, that text counts as your intent too. The fix the documentation gives is a human checkpoint: "Configure always_ask on the tools you would not let that end user run without review." Changing a policy only affects sessions created after the change, because running sessions keep the configuration they started with.
Checkpoint 4 of 6· Match them up
Match each permission policy to how it involves a human
Tap a term, then the definition that fits it.
Only always_ask guarantees a human on every call. auto sends the human only the calls it can't decide on.
“When the server reaches no determination, the session pauses as it does under always_ask.”Source: platform.claude.com
Sources5
4.The confirmation round-trip
When a call evaluates to ask, the session emits the tool-use event and goes idle with stop_reason.type set to requires_action. The blocking event IDs are listed in stop_reason.event_ids, and the session "waits indefinitely for a response." Your client answers each blocking event with a user.tool_confirmation. Once every one is resolved, the session goes back to running. Denials should explain themselves, because the agent receives your deny_message in its tool result and can choose a better path:
# Or deny it with an explanation
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.tool_confirmation",
"tool_use_id": mcp_tool_use_event.id,
"result": "deny",
"deny_message": "Don't create issues in the production project. Use the staging project.",
},
],
)Each tool-use event also carries evaluated_permission (allow, ask or deny), so you have a record of which calls reached a human. A reason_code is for your client to branch on and "keep in audit records, not text to display to end users."
Checkpoint 5 of 6· Put it in order
Put the steps of an always_ask confirmation in order
- 1.The session returns to running; allowed tools execute and denied tools return your deny_message
- 2.The session emits an agent.tool_use or agent.mcp_tool_use event
- 3.Your client sends a user.tool_confirmation with the event's ID as tool_use_id and result allow or deny
- 4.The session goes idle with stop_reason.type requires_action
The pause comes first, then your confirmation for each blocking event, then the session resumes once all of them are resolved.
“Send a user.tool_confirmation event for each blocking event, passing the event ID in the tool_use_id parameter.”Source: platform.claude.com
Checkpoint 6 of 6· Exam question
An engineering lead wants to use a Claude agent to investigate a suspected security vulnerability across a large codebase, but insists that no file be modified until she has personally reviewed and approved the exact diff Claude intends to make. Which configuration best matches this workflow?
Correct answer: A — Run the session with permissionMode "plan" so Claude can freely use read-only tools to investigate, while any file-edit or shell-write tool it proposes is routed to the canUseTool callback for her review, regardless of matching allow rules.
- A. Correct. plan mode lets Claude investigate freely with read-only tools while never auto-approving file edits, so every proposed change routes to her for review before anything is written, matching the requirement exactly.
- B. Incorrect. acceptEdits writes files immediately without a pre-write review step, which violates the requirement that no file change happen before her explicit approval.
- C. Incorrect. Manually deny-listing and unlisting every source path is impractical at scale and does not give per-diff review of the exact content of each proposed change.
- D. Incorrect claim. Deny rules are also evaluated before the permission-mode step, not just ask rules, so the statement misdescribes the evaluation order, and this setup is a needlessly risky way to reach the same outcome as plan mode.
Sources5
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.Under the auto policy, a human operator can rescue a server denial by sending a user.tool_confirmation with result allow.Why is that wrong?
High-risk denials under auto are final. A confirmation for any event whose evaluated_permission isn't ask gets a 400 error.
Covered in Managed Agents: always_allow, always_ask and auto
2.Switching a tool to always_ask on the agent immediately puts a checkpoint on sessions that are already running.Why is that wrong?
Policy updates only apply to sessions created after the update.
Covered in Managed Agents: always_allow, always_ask and auto
3.Setting always_ask on the agent's tools also gates the custom tools your application defines.Why is that wrong?
Permission policies don't cover custom tools. Your application runs them, so you have to add any human checkpoint yourself.
Covered in Managed Agents: always_allow, always_ask and auto
Practise it for real
Put bash behind a human checkpoint in a Managed Agent and practise denying a call with guidance
1.Create an agent whose agent toolset defaults to always_allow, with a configs entry setting bash to always_ask
Why: This puts a human checkpoint on the riskiest tool and leaves the others running freely
You should see: The agent is created with bash set to always_ask
2.Start a new session and ask the agent to do something that needs a shell command
Why: Only sessions created after the configuration change pick it up
You should see: An agent.tool_use event for bash, followed by session.status_idle with stop_reason.type requires_action
3.Send a user.tool_confirmation with the event's ID as tool_use_id, result deny, and a deny_message describing a safer approach
Why: A deny with a reason redirects the agent instead of just stopping it
You should see: The session returns to running, and the agent receives a tool result saying the call was rejected, including your deny_message
Stuck? Get a nudge
Look up the blocking event IDs in stop_reason.event_ids on the idle event.
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.https://code.claude.com/docs/en/agent-sdk/user-inputOfficial docs
“auto-approved by a permission rule or permission mode”
↩︎ canUseTool: sending tool calls to a person“Check if tool_name == "AskUserQuestion" to handle it differently.”
↩︎ canUseTool: sending tool calls to a person“Approve with changes: modify the input before execution (for example, sanitize paths, add constraints)”
↩︎ canUseTool: sending tool calls to a person“Suggest alternative: block but guide Claude toward what the user wants instead”
↩︎ canUseTool: sending tool calls to a person“Required workaround: dummy hook keeps the stream open for can_use_tool”
↩︎ Hooks as a second checkpoint layer - 2.https://code.claude.com/docs/en/permissionsOfficial docs
“Ask rules prompt for confirmation whenever Claude Code tries to use the specified tool.”
↩︎ canUseTool: sending tool calls to a person“Auto-denies every call that would otherwise prompt”
↩︎ canUseTool: sending tool calls to a person - 3.https://code.claude.com/docs/en/permission-modesOfficial docs
“Claude Code blocks edits until you approve a plan”
↩︎ canUseTool: sending tool calls to a person - 4.https://code.claude.com/docs/en/agent-sdk/hooksOfficial docs
“Require human approval for sensitive actions like database writes or API calls”
↩︎ Hooks as a second checkpoint layer“Matchers filter which hooks run”
↩︎ Hooks as a second checkpoint layer - 5.
“Each toolset kind has its own default: the agent toolset defaults to always_allow, and MCP toolsets default to always_ask.”
↩︎ Managed Agents: always_allow, always_ask and auto“This ensures that new tools added to an MCP server do not execute in your application without approval.”
↩︎ Managed Agents: always_allow, always_ask and auto“Configure always_ask on the tools you would not let that end user run without review.”
↩︎ Managed Agents: always_allow, always_ask and auto“If you relay untrusted end-user input in user.message events, the server reads that input as your intent too”
↩︎ Managed Agents: always_allow, always_ask and auto“The session waits indefinitely for a response.”
↩︎ The confirmation round-trip“Denied tools do not run, and the agent receives a tool result saying the call was rejected, including your deny_message.”
↩︎ The confirmation round-trip“A reason_code is a value for your client to branch on and keep in audit records, not text to display to end users.”
↩︎ The confirmation round-trip“The session keeps running, and your client cannot override the denial.”
↩︎ Exam trap 1“Running sessions keep the toolset configuration they were created with.”
↩︎ Exam trap 2“Custom tools are executed by your application and controlled by you, so they are not governed by permission policies.”
↩︎ Exam trap 3“The session keeps running, and your client cannot override the denial.”
↩︎ Prediction“When the server reaches no determination, the session pauses as it does under always_ask.”
↩︎ Checkpoint“Send a user.tool_confirmation event for each blocking event, passing the event ID in the tool_use_id parameter.”
↩︎ Checkpoint