Skip to content

Claude Managed Agents agent guard

Claude Managed Agents is Anthropic’s hosted agent harness. Anthropic runs the built-in toolset (bash, files, and similar) in its environment. Arcjet Guard sits at the boundaries your application still holds: inbound user.message before you send it, and custom tools on agent.custom_tool_use before your app executes them.

What is Arcjet? Arcjet is the runtime security platform that ships with your code. Enforce budgets, stop prompt injection, detect bots, and protect personal information with Arcjet's AI security building blocks.

Use protect() on HTTP routes. Use the helpers on this page for agent tools and other actions that have no HTTP request.

You need an Arcjet account and an ARCJET_KEY. Launch one client at module scope and reuse it.

Framework wrappers take an action string such as email.sent. That slug selects the matching remote policy and names the event in the Arcjet Console. Direct guard() calls use the field name label for the same slug. Don’t pass label to a wrapper such as guardTool().

You can submit SDK rules in code, rely on a published remote policy, or combine both. For more information about the decision model, see Agent guards.

This adapter is not the Claude Agent SDK. The Agent SDK is a local query() loop with PreToolUse hooks. For that product, see Claude Agent SDK agent guard and Claude Agent SDK Python agent guard.

Vercel AI SDK, LangChain, CrewAI, Eve, Mastra, and other wrappers are on Framework integrations.

Anthropic owns the session and executes built-in tools. The agent toolset defaults to always_allow. Your application does not get a pre-exec hook for bash, files, or other built-ins before they run.

The gates you can enforce are the following:

  • Inbound user.message: Screen the prompt with guardEvents / guard_events before you call events.send. On DENY, don’t send the event. The model never sees the prompt.
  • Custom tools on agent.custom_tool_use: Wrap the handler your app runs with guardCustomTool / guard_custom_tool. On DENY, the handler does not run. Return the denial as user.custom_tool_result with is_error set. is_error is on the events schema. Don’t throw.

always_ask plus user.tool_confirmation is opt-in. It is not the default. It is human-in-the-loop (HITL) confirmation, not policy. Don’t wrap confirmation as Guard.

For MCP, Anthropic is the MCP client. Put Guard on MCP servers you host. You cannot intercept an MCP call that Anthropic executes against a server you don’t run.

The peer packages are @anthropic-ai/sdk (>=0.86.0 <1, JavaScript) and anthropic>=0.92.0,<2 (Python). Don’t install claude-agent-sdk or @anthropic-ai/claude-agent-sdk for this adapter.

Until @arcjet/guard/claude-managed-agents/v0 is published, pin @arcjet/guard to cb35c8f9. Published @arcjet/guard does not export ./claude-managed-agents/v0.

Terminal window
npm install @arcjet/guard@git+https://github.com/arcjet/arcjet-js.git#cb35c8f92c3a2fb63fbeb9b386d79b1878c19d92 @anthropic-ai/sdk

@anthropic-ai/sdk (>=0.86.0 <1) is an optional peer, not a dependency of @arcjet/guard. If your project already has it, install @arcjet/guard on its own so your pins don’t move. The peer is @anthropic-ai/sdk, not @anthropic-ai/claude-agent-sdk.

Import helpers from the versioned path @arcjet/guard/claude-managed-agents/v0. There is no unversioned alias. @arcjet/guard/claude-managed-agents does not resolve. The Managed Agents API is pre-1.0, so the segment is v0. 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! });

Pick the surface that matches what you hold when the effect runs:

You haveJavaScriptPythonBlocks a call?
Inbound user text, before events.sendguardEventsguard_eventsYes
A custom tool your app executesguardCustomToolguard_custom_toolYes
A caller-owned session IDclaudeManagedAgentsContextclaude_managed_agents_contextNo

guardEvents / guard_events screens inbound text on user.message before you send it. There is no guardInbound and no UserPromptSubmit hook. Anthropic does not expose a local pre-prompt hook on this product.

guardCustomTool / guard_custom_tool wraps the handler you run when the session emits agent.custom_tool_use. On DENY, or when Guard cannot be evaluated and onGuardError / on_guard_error is "deny", the handler does not run. Return the denial as user.custom_tool_result with is_error set. is_error is on the events schema. Don’t invent a second field. Don’t throw. A throw is a raw exception.

claudeManagedAgentsContext / claude_managed_agents_context reads a caller-owned session ID. It never mints an ID. It never calls createAgentContext. Pass the id Anthropic returned from sessions.create. If you omit a session ID, the call is uncorrelated rather than joined to a generated ID.

There is no guardTool, guardHooks, PreToolUse, or canUseTool on this adapter. Those names belong to the Claude Agent SDK. Don’t wrap these tools with @arcjet/guard/claude-agent-sdk/v0 or arcjet.guard.claude_agent_sdk.

OptionHelpersDescription
actionguardCustomTool, guardEventsGuard label and capture name. Use resource.verb in the past tense, such as email.sent.
rulesguardCustomTool, guardEventsSDK rules, or a function of the tool input or { prompt }. Omit to submit none.
inboundguardEventsPolicy for user.message. Uses { prompt } in rules.
sessionIdBothCaller-owned Anthropic session id. Never minted.
onGuardErrorBoth"deny" (default) or "allow".

There is no guardInbound and no UserPromptSubmit. Screen prompt injection on guardEvents / guard_events inbound before you send user.message. This is the only place a turn can be declined before the model sees the prompt.

On DENY, don’t send the event. The model never sees the prompt.

Helpers default to onGuardError: "deny" / on_guard_error="deny". "allow" is a legitimate choice on inbound, because failing closed there stops the agent answering during an outage.

Pass the Anthropic session id that you already have. Don’t mint one. If you omit a session ID, the call is uncorrelated.

import Anthropic from "@anthropic-ai/sdk";
import { detectPromptInjection } from "@arcjet/guard";
import { guardEvents } from "@arcjet/guard/claude-managed-agents/v0";
import { arcjet } from "./arcjet.js";
const client = new Anthropic();
export async function sendTurn(
sessionId: string,
userText: string,
) {
await guardEvents(arcjet, {
sessionId,
inbound: {
action: "message.received",
rules: ({ prompt }) => [detectPromptInjection()(prompt)],
},
prompt: userText,
});
await client.beta.sessions.events.send(sessionId, {
events: [
{
type: "user.message",
content: [{ type: "text", text: userText }],
},
],
});
}

Replace sessionId with the id returned by client.beta.sessions.create.

Gate custom tools on agent.custom_tool_use

Section titled “Gate custom tools on agent.custom_tool_use”

Your application executes custom tools. Anthropic does not. Permission policies do not apply to them. When the session emits agent.custom_tool_use, wrap the handler you are about to run.

On DENY the handler does not run. Send user.custom_tool_result with is_error set and the denial payload in the result text so the model can inspect it. is_error is on the events schema. Don’t throw. A throw is a raw exception.

Scan free-text args (a note, reason, or body). An opaque orderId / order_id does not trip email, phone, card, or IP detection, so don’t pass it to localDetectSensitiveInfo / LocalDetectSensitiveInfo. That helper runs on a local ML model backend.

import { guardCustomTool } from "@arcjet/guard/claude-managed-agents/v0";
import { tokenBucket, localDetectSensitiveInfo } from "@arcjet/guard";
import { arcjet } from "./arcjet.js";
const lookupLimit = tokenBucket({
bucket: "lookups",
refillRate: 10,
intervalSeconds: 60,
maxTokens: 10,
});
const detectPii = localDetectSensitiveInfo();
export const lookupOrder = guardCustomTool(
arcjet,
async (input: { orderId: string; note: string }) => {
return { orderId: input.orderId, status: `shipped (${input.note})` };
},
{
action: "order.looked-up",
rules: (input) => [
lookupLimit({ key: input.orderId, requested: 1 }),
detectPii(input.note),
],
},
);

Call lookupOrder from your agent.custom_tool_use handler. Then send user.custom_tool_result with custom_tool_use_id set to the triggering event id.

Built-in tools (agent.tool_use) already ran when you see the event, unless you opted into always_ask. That confirmation path is HITL, not a Guard deny.

The agent toolset defaults to always_allow. Built-in bash and file tools run in Anthropic’s environment with no customer pre-exec.

always_ask pauses the session for user.tool_confirmation. That callback is HITL confirmation, not policy. Allowed-tool lists and confirmation results can skip or approve a built-in after the fact. There is no guardApproval and no canUseTool on this adapter.

Use guardCustomTool / guard_custom_tool for tools your app executes. Don’t put Arcjet policy on user.tool_confirmation.

Anthropic is the MCP client. When a session calls an MCP tool, Anthropic connects to the server. You don’t get a local PreToolUse hook.

If you host the MCP server, put Guard inside that server’s tool handlers. If Anthropic reaches a server you don’t run, you cannot deny the call from this adapter.

Claude Managed Agents helpers default to onGuardError: "deny" / on_guard_error="deny". If Guard cannot be evaluated, inbound user.message is not sent and the custom-tool handler does not run.

Set onGuardError: "allow" or on_guard_error="allow" only when you can accept running the action without a complete security decision. "allow" is a legitimate choice on inbound user.message because failing closed there stops the agent answering during an outage.

A DENY conclusion always blocks, regardless of onGuardError / on_guard_error.

The core guard() call still fails open. The wrappers that sit around an effect fail closed.

For more information about fail-open versus fail-closed behavior, see Availability and fail behavior.

claudeManagedAgentsContext / claude_managed_agents_context reads the caller-owned session ID you pass. It never mints an ID. It never reads a generated request or trace ID. If you omit a session ID, the call is uncorrelated rather than joined to a generated ID.

Use the id Anthropic returned from sessions.create. Don’t generate a UUID for Arcjet and don’t call createAgentContext inside a session callback.

const session = await client.beta.sessions.create({
agent: AGENT_ID,
environment_id: ENVIRONMENT_ID,
});
await guardEvents(arcjet, {
sessionId: session.id,
inbound: {
action: "message.received",
rules: ({ prompt }) => [detectPromptInjection()(prompt)],
},
prompt: userText,
});

Replace the following:

  • AGENT_ID: the agent id returned by agents.create.
  • ENVIRONMENT_ID: the environment id returned by environments.create.
  • There is no guardInbound. Screen prompt injection on guardEvents / guard_events inbound before user.message.
  • There is no guardApproval and no canUseTool. always_ask plus user.tool_confirmation is HITL confirmation, not policy.
  • There is no PreToolUse and no query(). Those belong to the Claude Agent SDK.
  • Don’t expect a customer pre-exec hook for built-in bash or files. The default is always_allow.
  • Don’t throw from guardCustomTool / guard_custom_tool to signal a denial. Return user.custom_tool_result with is_error set.
  • Don’t mint a session ID. Pass the Anthropic session id. If you omit one, leave the call uncorrelated.
  • Don’t wrap these tools with @arcjet/guard/claude-agent-sdk/v0 or arcjet.guard.claude_agent_sdk.
  • Don’t wrap these tools with @arcjet/guard/vercel-ai/v7.
  • Don’t import @arcjet/guard/claude-managed-agents. The path is @arcjet/guard/claude-managed-agents/v0.
  • Prompt injection before the model sees the prompt: guardEvents / guard_events with inbound rules calling detectPromptInjection()(prompt) / DetectPromptInjection().
  • Rate limit plus PII on a custom tool: guardCustomTool / guard_custom_tool with tokenBucket / TokenBucket and localDetectSensitiveInfo() / LocalDetectSensitiveInfo() on a free-text note.
  • MCP tools on a server you host: Guard inside that server’s handlers. Anthropic is the MCP client.