The canUseTool Callback
Programmatic control over every tool call. Allow, deny, modify inputs, and log tool usage with fine-grained precision that permission modes alone cannot provide.
What the canUseTool callback does
Permission modes work at a category level. acceptEdits auto-approves file writes. bypassPermissions approves everything. But what if you want fine-grained control — approve this tool, deny that one, allow the write tool but only to a specific file path?
The canUseTool callback gives you that control. It fires for every tool call that is not already auto-approved by your permission mode or hooks. You get the tool name and its inputs. You decide whether it runs, whether it is denied, and optionally what modified inputs it runs with.
This is the programmatic equivalent of the approval prompts you see in Claude Code — the same mechanism, now accessible in your own code.
Basic setup
for await (const message of query(messages(), {
model: "claude-sonnet-4-5",
permissionMode: "default",
canUseTool: async ({ toolName, input }) => {
console.log(`Tool requested: ${toolName}`);
console.log(`Input:`, input);
return { behavior: "allow" };
},
})) {
// handle messages
}
Running in default mode means every tool call routes through the callback. The toolName is the name of the tool being requested. The input is the full set of parameters the agent is trying to pass to that tool.
The callback returns a behavior object with three options:
{ behavior: "allow" }— let it run{ behavior: "deny", message: "Reason for denial" }— block it, tell the agent why{ behavior: "allow", updatedInput: { ...input, filePath: "new-path.md" } }— run it with modified inputs
Denying specific tools
canUseTool: async ({ toolName, input }) => {
if (toolName === "web_search" || toolName === "web_fetch") {
return {
behavior: "deny",
message: "Web access is not allowed. Use your existing knowledge.",
};
}
return { behavior: "allow" };
}
The denial message is not just a log entry. It gets sent back to the agent as context. The agent uses it to adjust its approach — in this case, stopping attempts to search the web and relying on its training data instead.
Logging tool inputs for audit trails
Each tool has a different input shape. The web_search tool has a query field. The write tool has a filePath and content field. The bash tool has a command field.
canUseTool: async ({ toolName, input }) => {
// Log every tool call with its inputs
auditLog.push({
timestamp: new Date().toISOString(),
tool: toolName,
input: JSON.stringify(input),
});
return { behavior: "allow" };
}
As you build more agents, you develop a feel for which inputs each tool exposes. The callback gives you access to all of them, which lets you make precise decisions — not just "allow web search" but "allow web search only when the query contains a client name."
Modifying tool inputs before execution
canUseTool: async ({ toolName, input }) => {
if (toolName === "write") {
// Redirect all file writes to a sandboxed directory
return {
behavior: "allow",
updatedInput: {
...input,
filePath: `/sandbox/${input.filePath}`,
},
};
}
return { behavior: "allow" };
}
This redirects every file write to a /sandbox/ directory, regardless of what path the agent originally specified. Input modification is powerful — and worth using carefully. The agent does not know its inputs were changed, so the behavior can be surprising if you modify too aggressively.
Combining with permission modes
The callback works in any permission mode, but its behavior varies:
- In
defaultmode: fires for every tool call - In
acceptEditsmode: fires for tools that are not file operations - In
bypassPermissionsmode: never fires, everything is auto-approved
For agents running in production without supervision, acceptEdits mode combined with a callback for non-file tools gives a good balance: file work runs automatically, everything else gets logged or selectively approved.
> Operator use case: Build a client research agent that allows web searches but blocks file writes. Or allow file writes only to a specific client folder. Or log every bash command to a compliance record. The canUseTool callback is where those guardrails live — not in the model's instructions, but in code that the agent cannot override.
---
Author: FractionalSkill