AI agent security

canUseTool is not a policy gate

Claude's canUseTool looks like a gate. allowedTools, allow rules, and bypassPermissions / acceptEdits skip it. A Bash or Write in allowedTools never hits the callback. Those are different jobs.

4 min read
In short: Claude's canUseTool looks like a gate. allowedTools, allow rules, and bypassPermissions / acceptEdits skip it. A Bash or Write in allowedTools never hits the callback. Those are different jobs.

What is canUseTool?

canUseTool is the Claude Agent SDK stand-in for the interactive permission prompt. Anthropic's permissions documentation says so: the callback runs when no earlier step resolved the call. It is the ask path, not a check on every tool.

That is the same class of trap as Eve approval and Mastra requireApproval. A person, or a function that stands in for a person, sees the calls that weren't already approved. See human approval gates. Rubber-stamping every leftover call isn't a policy.

When does canUseTool not run?

Anthropic is explicit in the permissions documentation: auto-approved tools never reach canUseTool. A bare allowedTools entry such as Bash or Write auto-approves every call unless an ask rule or plan mode sends it back to the callback. bypassPermissions approves everything that reaches the mode step. Both have narrow documented exceptions, such as tools that require user interaction and critical-path rm or rmdir. acceptEdits approves Write, Edit, and file system operations (mkdir, rm, mv) inside the working directory.

The TypeScript SDK warns once when you pass a callback that the evaluation order can never reach. The code is CLAUDE_SDK_CAN_USE_TOOL_SHADOWED. Two configurations trigger it: permissionMode: "bypassPermissions", and each bare allowedTools name.

allowedTools doesn't constrain bypassPermissions. Unlisted tools fall through to the mode, which approves them. allowedTools: ["Read"] plus bypassPermissions still approves Bash and Write.

A scoped rule such as Bash(ls *) is different. Matching calls are approved; other Bash calls can still reach the callback. A deny rule such as Bash(rm *) blocks matching calls in every mode, including bypassPermissions.

Are permission modes and annotations a gate?

Permission modes are a global auto-approve, or a deny instead of a prompt. They aren't a labeled decision on this tool with application context.

readOnlyHint is metadata. Anthropic's custom tools page says that annotations aren't enforcement. A tool marked readOnlyHint: true can still write to disk if that is what the handler does.

Sandbox settings isolate the file system and network for Bash. They don't run on Read, Edit, or Write, and they don't authorize this MCP call. See A sandbox is not a tool policy for that split on Eve.

What actually denies a tool?

Hooks run first. A PreToolUse hook can return permissionDecision: "deny" and skip the tool, including under bypassPermissions. PostToolUse runs after the tool. It can't un-send.

That is the last reversible point. The lethal trifecta still applies: private data, untrusted content, and external communication don't have to look like a jailbreak, and they don't have to pass through canUseTool.

On Claude Agent SDK, Arcjet Guards put that deny on the tool. Authored tools wrap with guardTool from @arcjet/guard/claude-agent-sdk/v0. A deny is a CallToolResult with isError: true, not a throw. Built-in tools and the MCP tools that you didn't wrap go through PreToolUse with guardHooks. There is no inbound helper on the query itself; screen the prompt on UserPromptSubmit.

Mastra guardrails vs an action gate is the same job on a different framework. AI agent runtime security is the job.

When do you need both?

Keep Claude's allow and deny rules for the prompt surface. Put a deny on send, write, Bash, and every MCP tool that can leave the trust boundary.

The callback never sees a Bash call that is already on the list.

Frequently asked questions

If I set bypassPermissions or acceptEdits, is canUseTool still a gate?

No. Those modes approve the call before the callback runs. The TypeScript SDK warns when the callback is shadowed.

If Bash is in allowedTools, does my canUseTool still see it?

No. A bare name auto-approves every call. Only a scoped rule such as Bash(ls *) leaves other Bash calls for the callback.

Can PostToolUse undo a send?

No. The tool already ran. PreToolUse is the last reversible point.

Does Arcjet replace Claude permissions?

No. Claude's rules shape the prompt surface. An action gate is a deny on this tool. Use both.

AI runtime security in your code

Protect your AI agent workflows with Arcjet

Get allow, deny, and redact on agent actions before the side effect.