The short answer
In the Claude Agent SDK, a tool call passes six steps in order: hooks, deny rules, ask rules, the permission mode, allow rules, and finally your canUseTool callback.1 canUseTool is the last step, so it only sees calls nothing earlier resolved. The docs warn that a call approved earlier, by an allow rule, acceptEdits or bypassPermissions, skips your callback, “so permission checks you put there are silently bypassed for that tool”.1
A PreToolUse hook is the first step. It runs before everything else, and a hook deny applies even in bypassPermissions mode.1 The rule of thumb: use canUseTool to ask a person, and a PreToolUse hook for any check that must run on every call.
Deedglass
A tool call meets your rules before it runs
Asks to run
DELETE FROM visits;
Deletes every row in visits
Deedglass checks your rules
- Reading dataAllowed
- Deleting a whole table (DROP, TRUNCATE)Never
- Changing or deleting rows without saying which (WHERE)This one
- Other changesWait for a person
The six steps
The docs give the evaluation order, and each step has a catch worth knowing.1
| Step | What happens | The catch |
|---|---|---|
| 1. Hooks | A hook can deny, ask, allow or pass | A hook allow doesn’t skip the deny and ask rules below |
| 2. Deny rules | disallowed_tools and settings deny rules block the call |
Holds even in bypassPermissions and after a hook allow |
| 3. Ask rules | Settings ask rules send the call to canUseTool |
Holds even in bypassPermissions |
| 4. Permission mode | bypassPermissions approves, acceptEdits approves file operations, plan sends edits to the callback |
Approval here means the callback never runs |
| 5. Allow rules | allowed_tools and settings allow rules approve |
A bare tool name approves every call to that tool |
6. canUseTool |
Your callback decides | Skipped entirely in dontAsk mode, which denies instead |
Our framework comparison put the SDK next to eight others;1 this post goes deeper on the two hooks you write yourself.
What canUseTool is for
canUseTool is the approval prompt. It fires when Claude wants a tool that no rule or mode has approved, and when Claude asks a clarifying question through AskUserQuestion.2 It receives the tool name, the tool’s input (for Bash, command, description and timeout) and a context object with optional suggestions for rules that would avoid prompting again.2
It returns one of two answers:2
Code Program code, for developers. The text around it says what it does.
canUseTool: async (toolName, input) => {
if (toolName === "Bash" && /\bpsql\b/.test(String(input.command))) {
const ok = await askReviewer(toolName, input); // your own UI
if (!ok) return { behavior: "deny", message: "Reviewer declined the database command" };
}
return { behavior: "allow", updatedInput: input };
}
Three details from the docs matter in production:
- It can wait forever. Execution stays paused until the callback returns. If a person may take longer than your process can stay up, the docs suggest a
PreToolUsehook returningdefer, so the process can exit and resume later.2 - You can change the input. Returning a modified
updatedInputruns the tool with your version, and Claude isn’t told you changed anything.2 That’s useful for scoping a path, and a reason to log what you changed. - “Always allow” writes a rule. Echoing a suggestion with the
localSettingsdestination back inupdatedPermissionswrites the rule to.claude/settings.local.json, so future sessions skip the prompt.2 That rule then approves calls at step 5, and your callback won’t see them again.
In Python, the docs’ examples pass the prompt as a stream and register a no-op PreToolUse hook, labelled a “required workaround” that keeps the stream open for can_use_tool.2 If your Python callback never fires, check that first.
What a PreToolUse hook is for
A hook is code that runs before the permission flow. You register it with a matcher on the tool name, and it returns a decision in hookSpecificOutput.permissionDecision: allow, deny, ask or defer, plus a reason Claude sees.3
Code Program code, for developers. The text around it says what it does.
async def block_destructive_sql(input_data, tool_use_id, context):
cmd = input_data["tool_input"].get("command", "")
if re.search(r"\b(drop|truncate)\b", cmd, re.I):
return {"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive SQL needs a human; open a ticket"}}
return {}
options = ClaudeAgentOptions(
hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[block_destructive_sql])]})
How hooks combine and fail, from the docs:3
| Behaviour | What the docs say |
|---|---|
| Several hooks match | They run in parallel; the most restrictive result wins |
| Priority | deny beats defer, which beats ask, which beats allow |
| Default timeout | 600 seconds for PreToolUse, set per matcher with timeout |
| Hook times out | The tool call is blocked and the turn continues |
| Matchers | Match tool names only, not paths or arguments |
The timeout behaviour is the safe one: a PreToolUse callback that doesn’t answer blocks the call.3 Our post on Copilot’s preToolUse hooks shows that not every agent behaves this way.
Four configurations that skip your callback
These come straight from the docs, and each one quietly turns canUseTool into dead code for some calls.1
bypassPermissionsmode. Everything that reaches step 4 is approved. The TypeScript SDK emits a process warning, codeCLAUDE_SDK_CAN_USE_TOOL_SHADOWED, when you pass a callback with this mode.- A bare tool name in
allowedTools."Bash"approves every Bash call at step 5. The same warning lists such entries. A scoped rule such asBash(npm test *)approves only matching calls and doesn’t trigger it. acceptEditsmode. File edits and commands such asrm,mvandsedinside the working directory are approved at step 4.dontAskmode. Step 6 is skipped and the call is denied. That’s the right choice for a headless agent, but your callback won’t run.
One more trap: allowed_tools does not constrain bypassPermissions. Setting allowed_tools=["Read"] with bypassPermissions still approves Bash, Write and Edit. To block specific tools in that mode, use disallowed_tools.1
A pattern that holds up
For an agent that can touch anything that matters:
- Hard rules in a
PreToolUsehook. Destructive SQL, production hosts, secrets files. The hook runs first and its deny holds in every mode.1 - Hide what the agent never needs. A bare name in
disallowed_toolsremoves the tool from the request, so Claude can’t attempt it.1 - Scoped allow rules for the routine.
Bash(npm test *), notBash. canUseToolfor the judgement calls. It sees what’s left: calls that need a person.dontAskfor headless runs. With no one to ask, a hard deny beats a callback that waits forever.1- Log decisions outside the agent. Record what each hook and callback decided, including any
updatedInputyou rewrote.
Limits of both
Both check the request, not the result. A hook sees a command string; it doesn’t see what the database did with it. A Write|Edit matcher doesn’t run when Claude writes the same file with Bash; the docs say so in their own .env example.3 Regex on shell commands misses aliases, variables and scripts; our post on why regex can’t block DROP TABLE shows how. And a hook with an allow can’t approve recognised critical-path removals such as rm -rf ~, which is a backstop, not a policy.1
For anything with side effects outside the machine, pair the hook with a check on the target system itself: a database role with only the rights the agent needs, and a record of what actually changed.
Questions people ask
Why isn’t my canUseTool callback being called?
Something earlier approved the call: an allow rule (often a bare tool name), acceptEdits, bypassPermissions, or a saved “always allow” rule. In dontAsk mode it’s never called.1 In Python, check the streaming-input workaround.2
Can a PreToolUse hook approve a call?
Yes, by returning allow, but deny and ask rules are still evaluated after it.1
Which should I use for human approval?
canUseTool, for calls that reach it. For approvals that must cover every call of a tool, have a hook return ask or defer.3
To see which agents and MCP servers on your machine can reach a database, run npx deedglass scan (the grade, explained). To watch approvals and read-backs on a real database in your browser, open the playground.
Written with AI assistance and checked against our claims register.