UserPromptSubmit and deny tools at PreToolUse. Wrap authored tool() with guardTool, and use guardHooks for inbound text and unwrapped built-ins; canUseTool is not a policy gate.How do you secure a Claude Agent SDK agent?
Screen the prompt before the model sees it. Deny the tool before it runs. Those are two jobs.
guardTool wraps an authored tool(). guardHooks screens inbound text on UserPromptSubmit and denies unwrapped built-in and MCP tools on PreToolUse. claudeAgentContext reads session_id. It never mints an ID.
protect() is the HTTP check on a route. The Claude Agent SDK agent guard documentation is the product map.
How do you screen inbound prompts?
Put detectPromptInjection on guardHooks({ inbound }). That hook is UserPromptSubmit. On deny, Claude Code erases the prompt. The model never sees it.
That is the only place a submitted prompt can be declined before it starts. A clean injection score doesn't authorize a write.
Helpers default to onGuardError: "deny". "allow" is a legitimate choice on inbound, because failing closed there would stop the agent from answering during an outage. Timeout already fail-closes the prompt.
Why isn't canUseTool a security policy?
Because Claude can skip it. allowedTools, allow rules, bypassPermissions, and acceptEdits approve the call first. canUseTool is not a policy gate is that trap.
Don't put the deny on canUseTool. Anthropic's permissions documentation says the same thing: the callback runs when no earlier step resolved the call.
How do you deny Bash or other unwrapped tools?
On PreToolUse, through guardHooks. Use that for Bash, Write, and the MCP tools that you didn't pass through guardTool. Anthropic's hooks run first. A hook that returns a deny decision (permissionDecision: "deny" in hookSpecificOutput) skips the tool, including under bypassPermissions. That is the slot that still sees a bare allowedTools name.
Keep Claude's own allow and deny rules for the prompt surface. They aren't a labeled decision on this tool with application context.
Authored tools wrap with guardTool. A deny is a CallToolResult with isError: true. It doesn't throw. Don't also wrap those tools with @arcjet/guard/vercel-ai/v7. Don't apply guardTool and PreToolUse to the same authored tool.
import { tool } from "@anthropic-ai/claude-agent-sdk";import { z } from "zod";import { guardTool } from "@arcjet/guard/claude-agent-sdk/v0";import { tokenBucket } from "@arcjet/guard";import { arcjet } from "./arcjet.js";
const lookupLimit = tokenBucket({ bucket: "lookups", refillRate: 10, intervalSeconds: 60, maxTokens: 10,});
export const lookupOrder = guardTool( arcjet, tool( "lookup_order", "Look up an order by ID", { orderId: z.string(), note: z.string(), }, async ({ orderId, note }) => ({ content: [{ type: "text", text: `${orderId}: shipped (${note})` }], }), ), { action: "order.looked-up", rules: (input) => [lookupLimit({ key: input.orderId, requested: 1 })], },);PostToolUse is capture only. It can't un-send.
Does detection replace an action gate?
No. Detection labels text. An action gate is a labeled allow or deny on this tool, with application context, immediately before the side effect.
Runtime security for LLM applications already splits prompt injection, exfiltration, and unsafe actions. A well-formed Write isn't a jailbreak. Screen the prompt. Deny the tool. Keep canUseTool for the calls that actually reach it.
Frequently asked questions
How do I secure a Claude Agent SDK agent?
Screen inbound text on UserPromptSubmit and deny tools at PreToolUse. Wrap authored tool() with guardTool. Use guardHooks for inbound prompts and for built-ins or MCP you did not wrap. claudeAgentContext reads session_id; it never mints one. protect() stays on HTTP routes.
Is canUseTool a policy gate?
No. Claude skips it when allowedTools, allow rules, or bypassPermissions / acceptEdits already approved the call. Put the deny on guardTool or PreToolUse. The companion article covers that trap.
Where do I put detectPromptInjection?
On guardHooks({ inbound }), which runs at UserPromptSubmit. That is the only place a turn can be declined before the model sees the prompt. Scanning the prompt does not deny Bash; PreToolUse or guardTool does.
What if Arcjet is down on UserPromptSubmit?
Helpers default to onGuardError: deny, so an unevaluated inbound check blocks the prompt. allow is a legitimate choice on inbound because failing closed there would stop the agent answering during an outage. Timeout already fail-closes the prompt.
Do I wrap the same tool with guardTool and PreToolUse?
No. That double-calls the guard. Use guardTool for authored tool(). Use PreToolUse via guardHooks for built-ins and MCP you did not wrap. Do not also wrap Claude tools with @arcjet/guard/vercel-ai/v7.
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.