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.

4 min read
canUseTool callback deny tool SDK modify tool input tool approval agent programmatic tool control

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 default mode: fires for every tool call
  • In acceptEdits mode: fires for tools that are not file operations
  • In bypassPermissions mode: 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

Keep Going

Ready to Start Building?

Pick the next step that matches where you are right now.

Tutorial
Claude Code Basics

Start with the terminal basics. A hands-on, step-by-step guide to your first 10 minutes with Claude Code.

Start the Tutorial
Guide
AI-Powered Workflows

Automate your client work. Learn how to connect AI tools into workflows that handle repetitive tasks for you.

Read the Guide
Community
Join the Community

Connect with other fractional leaders building with AI. Share workflows, get feedback, and learn from operators who are ahead of you.

Apply to Join