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.
Hosted harness
Section titled “Hosted harness”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 withguardEvents/guard_eventsbefore you callevents.send. OnDENY, don’t send the event. The model never sees the prompt. - Custom tools on
agent.custom_tool_use: Wrap the handler your app runs withguardCustomTool/guard_custom_tool. OnDENY, the handler does not run. Return the denial asuser.custom_tool_resultwithis_errorset.is_erroris 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.
Install
Section titled “Install”Until @arcjet/guard/claude-managed-agents/v0 is published, pin
@arcjet/guard to
cb35c8f9.
Published @arcjet/guard does not export ./claude-managed-agents/v0.
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! });Until arcjet.guard.claude_managed_agents is published, pin arcjet to
40ea4896.
Published arcjet 1.0.0 on PyPI does not include this extra.
pip install "arcjet[claude-managed-agents] @ git+https://github.com/arcjet/arcjet-py@40ea4896962a90a24cdbc4cfbfc80729c096da36"arcjet[claude-managed-agents] depends on anthropic>=0.92.0,<2.
CPython 3.10 or later. The peer is anthropic, not
claude-agent-sdk.
Import helpers from arcjet.guard.claude_managed_agents:
from arcjet.guard.claude_managed_agents import ( claude_managed_agents_context, guard_custom_tool, guard_events,)Launch one client at module scope:
from arcjet.guard import launch_arcjet
arcjet = launch_arcjet(key=ARCJET_KEY)Use launch_arcjet_sync with Flask, Django, or other sync code.
Helpers
Section titled “Helpers”Pick the surface that matches what you hold when the effect runs:
| You have | JavaScript | Python | Blocks a call? |
|---|---|---|---|
Inbound user text, before events.send | guardEvents | guard_events | Yes |
| A custom tool your app executes | guardCustomTool | guard_custom_tool | Yes |
| A caller-owned session ID | claudeManagedAgentsContext | claude_managed_agents_context | No |
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.
Helper options
Section titled “Helper options”| Option | Helpers | Description |
|---|---|---|
action | guardCustomTool, guardEvents | Guard label and capture name. Use resource.verb in the past tense, such as email.sent. |
rules | guardCustomTool, guardEvents | SDK rules, or a function of the tool input or { prompt }. Omit to submit none. |
inbound | guardEvents | Policy for user.message. Uses { prompt } in rules. |
sessionId | Both | Caller-owned Anthropic session id. Never minted. |
onGuardError | Both | "deny" (default) or "allow". |
guard_custom_tool accepts this contract:
| Option | Required | Description |
|---|---|---|
guard | Yes | Client from launch_arcjet or launch_arcjet_sync. |
action | Yes | Guard label and capture name. Use resource.verb in the past tense, such as email.sent. |
rules | No | Bound SDK rule inputs, or a function of the parsed tool arguments. Omit to submit none. |
session_id | No | Caller-owned Anthropic session id. Never minted. |
on_guard_error | No | "deny" (default) or "allow". A real DENY always blocks. |
guard_events accepts this contract:
| Option | Required | Description |
|---|---|---|
guard | Yes | Client from launch_arcjet or launch_arcjet_sync. |
action | No | Guard label and capture name. |
rules | No | Bound SDK rule inputs, or a function of { "prompt": ... }. Omit to submit none. |
inbound | No | Policy for user.message. Requires action. rules receives {"prompt": ...}. |
session_id | No | Caller-owned Anthropic session id. Never minted. |
on_guard_error | No | "deny" (default) or "allow". A real DENY always blocks. |
guard_events needs an inbound policy. There is no inbound
helper. Screen prompt injection on the events inbound path.
Screen inbound user.message
Section titled “Screen inbound user.message”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.
from anthropic import Anthropicfrom arcjet.guard import DetectPromptInjection, launch_arcjetfrom arcjet.guard.claude_managed_agents import guard_events
client = Anthropic()arcjet = launch_arcjet(key=ARCJET_KEY)inbound = DetectPromptInjection()
async def send_turn(session_id: str, user_text: str) -> None: await guard_events( guard=arcjet, session_id=session_id, inbound={ "action": "message.received", "rules": lambda arguments: [inbound(arguments["prompt"])], }, prompt=user_text, ) client.beta.sessions.events.send( session_id, events=[ { "type": "user.message", "content": [{"type": "text", "text": user_text}], } ], )Replace session_id 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.
from arcjet.guard import ( LocalDetectSensitiveInfo, TokenBucket, launch_arcjet,)from arcjet.guard.claude_managed_agents import guard_custom_tool
arcjet = launch_arcjet(key=ARCJET_KEY)lookup_limit = TokenBucket( refill_rate=10, interval_seconds=60, max_tokens=10, bucket="lookups",)detect_pii = LocalDetectSensitiveInfo()
async def lookup_order(arguments: dict) -> dict: return { "order_id": arguments["order_id"], "status": f"shipped ({arguments['note']})", }
guarded_lookup = guard_custom_tool( guard=arcjet, tool=lookup_order, action="order.looked-up", session_id=session_id, rules=lambda arguments: [ lookup_limit(key="orders", requested=1), detect_pii(arguments["note"]), ],)Call guarded_lookup from your agent.custom_tool_use handler.
Then send user.custom_tool_result with custom_tool_use_id set
to the triggering event id. On DENY, set is_error.
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.
Permission policies are not a policy gate
Section titled “Permission policies are not a policy gate”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.
MCP servers you host
Section titled “MCP servers you host”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.
Fail-closed default
Section titled “Fail-closed default”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.
Correlation
Section titled “Correlation”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 agentidreturned byagents.create.ENVIRONMENT_ID: the environmentidreturned byenvironments.create.
session = client.beta.sessions.create( agent=AGENT_ID, environment_id=ENVIRONMENT_ID,)
await guard_events( guard=arcjet, session_id=session.id, inbound={ "action": "message.received", "rules": lambda arguments: [inbound(arguments["prompt"])], }, prompt=user_text,)Replace the following:
AGENT_ID: the agentidreturned byagents.create.ENVIRONMENT_ID: the environmentidreturned byenvironments.create.
What not to use
Section titled “What not to use”- There is no
guardInbound. Screen prompt injection onguardEvents/guard_eventsinbound beforeuser.message. - There is no
guardApprovaland nocanUseTool.always_askplususer.tool_confirmationis HITL confirmation, not policy. - There is no
PreToolUseand noquery(). 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_toolto signal a denial. Returnuser.custom_tool_resultwithis_errorset. - 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/v0orarcjet.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.
Common patterns
Section titled “Common patterns”- Prompt injection before the model sees the prompt:
guardEvents/guard_eventswith inboundrulescallingdetectPromptInjection()(prompt)/DetectPromptInjection(). - Rate limit plus PII on a custom tool:
guardCustomTool/guard_custom_toolwithtokenBucket/TokenBucketandlocalDetectSensitiveInfo()/LocalDetectSensitiveInfo()on a free-textnote. - MCP tools on a server you host: Guard inside that server’s handlers. Anthropic is the MCP client.
Related
Section titled “Related”- Framework integrations
- Claude Agent SDK agent guard
(different product: local
query()/PreToolUse) - Claude Agent SDK Python agent guard
(different product: local
query()/PreToolUse) - Agent guards
- Example (JavaScript): examples/claude-managed-agents
- Example (Python): examples/fastapi-claude-managed-agents-guard
- Adapter JS:
cb35c8f9 - Adapter Python:
40ea4896