Claude Agent SDK agent guard
Claude Agent SDK agents
call authored tool() handlers, built-in tools, and MCP tools. Arcjet Guard
sits at those boundaries so a remote policy can allow or deny the action
before a side effect runs.
Use protect() for HTTP routes. Use the Claude helpers on this page for authored tools,
inbound prompts, and unwrapped built-in or MCP tools. Vercel AI SDK, LangChain,
Eve, and Mastra wrappers are on
Framework integrations.
Install
Section titled “Install”Install the Guard SDK and the Claude Agent SDK:
npm install @arcjet/guard @anthropic-ai/claude-agent-sdkImport helpers from the versioned path @arcjet/guard/claude-agent-sdk/v0.
There is no unversioned alias. @arcjet/guard/claude-agent-sdk does not
resolve. The Claude Agent SDK is pre-1.0, so the segment is v0.
@anthropic-ai/claude-agent-sdk is an optional peer (>=0.1.0 <1). The
integration requires Node.js 22 or later.
Launch one client at module scope:
import { launchArcjet } from "@arcjet/guard";
export const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });Helpers
Section titled “Helpers”The integration exposes three surfaces:
guardTool(client, toolDefinition, policy)wraps an authoredtool(). OnDENYit returns aCallToolResultwithisError: true. Do not throw.guardHooks()screens inbound text onUserPromptSubmit, denies unwrapped built-in and MCP tools onPreToolUse, and captures results onPostToolUse. Capture cannot undo a tool that already ran.claudeAgentContext()readssession_idfrom hook input oroptions.sessionId. It never mints an id. Subagentagent_idis metadata only.
There is no guardInbound. Screen prompt injection on
guardHooks({ inbound }) via UserPromptSubmit. There is no
guardCanUseTool and no guardApproval.
Never call createAgentContext inside a Claude callback. Do not also wrap
these tools with @arcjet/guard/vercel-ai/v7.
The Claude Agent SDK integration skill ships with @arcjet/guard. After
npm install, copy or symlink it into your coding agent’s skills directory:
mkdir -p ~/.claude/skillscp -r node_modules/@arcjet/guard/skills/integrate-arcjet-guard-claude-agent-sdk ~/.claude/skills/Then ask the agent to add Arcjet Guard to this Claude Agent SDK project. In
Claude Code, run /integrate-arcjet-guard-claude-agent-sdk. The skill source is
integrate-arcjet-guard-claude-agent-sdk.
Screen inbound with UserPromptSubmit
Section titled “Screen inbound with UserPromptSubmit”There is no guardInbound. Screen prompt injection on
guardHooks({ inbound }) via UserPromptSubmit. This is the only place a
turn can be declined before the model sees the prompt.
On DENY, UserPromptSubmit returns { decision: "block", reason } and
Claude Code erases the prompt. The model never sees it.
Helpers default to onGuardError: "deny". "allow" is a legitimate choice on
inbound, because failing closed there stops the agent answering during an
outage. Timeout already fail-closes the prompt (Claude Code v2.1.208+).
import { query } from "@anthropic-ai/claude-agent-sdk";import { guardHooks } from "@arcjet/guard/claude-agent-sdk/v0";import { detectPromptInjection } from "@arcjet/guard";import { arcjet } from "./arcjet.js";
const sessionId = conversationId;
for await (const message of query({ prompt: userText, options: { sessionId, hooks: guardHooks(arcjet, { sessionId, inbound: { action: "message.received", rules: ({ prompt }) => [detectPromptInjection()(prompt)], }, }), },})) { void message;}canUseTool is not a policy gate
Section titled “canUseTool is not a policy gate”Claude’s canUseTool is skipped by allowedTools, allow rules, and
bypassPermissions / acceptEdits. That is the same trap as Eve approval and
Mastra requireApproval: the callback looks like a gate, but the runtime can
skip it.
There is no guardCanUseTool. Do not put Arcjet policy on canUseTool. Use
guardTool for authored tools or PreToolUse for unwrapped ones.
On DENY the tool’s handler never runs. The model receives a
CallToolResult with isError: true. Scan free-text args (a note, reason, or
body). An opaque orderId will not trip email / phone / card / IP, so do not
pass it to localDetectSensitiveInfo. That helper runs on a local ML model
backend.
import { tool } from "@anthropic-ai/claude-agent-sdk";import { z } from "zod";import { guardTool } from "@arcjet/guard/claude-agent-sdk/v0";import { tokenBucket, localDetectSensitiveInfo } from "@arcjet/guard";import { arcjet } from "./arcjet.js";
const lookupLimit = tokenBucket({ bucket: "lookups", refillRate: 10, intervalSeconds: 60, maxTokens: 10,});// Factory then text — same shape as `detectPromptInjection()(text)`.const detectPii = localDetectSensitiveInfo();
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 }), detectPii(input.note), ], },);PreToolUse is the only deny for unwrapped tools
Section titled “PreToolUse is the only deny for unwrapped tools”Built-ins (Bash, Write, …) and MCP tools not passed through guardTool are
gated here. PreToolUse returns permissionDecision: "deny". Timeout already
fail-closes, so the tool does not run.
Annotations (readOnlyHint) and sandbox settings are not enforcement.
PostToolUse is capture only and cannot undo a tool that already ran.
Do not apply guardTool and PreToolUse to the same authored tool. That
double-calls the guard.
import { guardHooks } from "@arcjet/guard/claude-agent-sdk/v0";import { detectPromptInjection, tokenBucket } from "@arcjet/guard";import { arcjet } from "./arcjet.js";
const mcpLimit = tokenBucket({ bucket: "mcp-access", refillRate: 20, intervalSeconds: 60, maxTokens: 20,});
export const hooks = guardHooks(arcjet, { sessionId: conversationId, action: ({ toolName }) => `${toolName}.invoked`, rules: ({ toolName }) => [mcpLimit({ key: toolName, requested: 1 })], inbound: { action: "message.received", rules: ({ prompt }) => [detectPromptInjection()(prompt)], },});Pass hooks to query({ options.hooks }). Use this for tools you did not
pass through guardTool.
Fail-closed default
Section titled “Fail-closed default”Claude Agent SDK helpers default to onGuardError: "deny". If Guard cannot be
evaluated, the tool does not run and inbound UserPromptSubmit blocks the
prompt. "allow" is a legitimate choice on inbound UserPromptSubmit because
failing closed there stops the agent answering during an outage. Timeout
already fail-closes the prompt (Claude Code v2.1.208+).
Correlation
Section titled “Correlation”Set options.sessionId on query() to a conversation identity you already
have. claudeAgentContext reads hook session_id first, then
options.sessionId. It never calls createAgentContext. If neither is a
valid 1–256 printable-ASCII string, the call is uncorrelated rather than
joined to a generated id. Subagent agent_id is metadata only.
const sessionId = conversationId;
for await (const message of query({ prompt: userText, options: { sessionId, hooks: guardHooks(arcjet, { sessionId }) },})) { void message;}What not to use
Section titled “What not to use”- There is no
guardInbound. Screen prompt injection onguardHooks({ inbound })viaUserPromptSubmit. - There is no
guardCanUseTool.canUseToolis not a policy gate. - There is no
guardApproval. - Do not call
createAgentContextinside a Claude callback. - Do not also wrap these tools with
@arcjet/guard/vercel-ai/v7. - Do not apply
guardToolandPreToolUseto the same authored tool. - Do not import
@arcjet/guard/claude-agent-sdk. The path is@arcjet/guard/claude-agent-sdk/v0.
Related
Section titled “Related”- Framework integrations
- Mastra agent guard
- Vercel Eve agent guard
- Agent guards
- Example: arcjet/examples#193
(
claude-agent)