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.
What is Arcjet?
Arcjet is the AI agent runtime security platform. Discover the agents running in your organization, enforce policy across every action, prompt, and tool call, and keep the evidence to prove what happened. Detect prompt injection, authorize agent tool calls, redact PII, and block bots and abuse.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 page covers both adapters. The JavaScript adapter is
@arcjet/guard/claude-agent-sdk/v0. The Python adapter is
arcjet.guard.claude_agent_sdk. They are separate packages that gate the same
boundaries, so pick the tab for the language your agent runs in and donât mix
the two.
Vercel AI SDK, LangChain, CrewAI, Eve, Mastra, and LangGraph 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.21.0 <23 || >=24.5.0.
Launch one client at module scope:
import { launchArcjet } from "@arcjet/guard";
export const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });pip install "arcjet[claude-agent-sdk]"The Python adapter requires arcjet 1.1.0 or later.
arcjet[claude-agent-sdk] depends on claude-agent-sdk>=0.2.127,<1.
CPython 3.10 or later.
Import helpers from arcjet.guard.claude_agent_sdk:
from arcjet.guard.claude_agent_sdk import ( claude_agent_context, guard_hooks, guard_tool,)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.
The integration exposes three surfaces:
guardTool(client, toolDefinition, policy)wraps an authoredtool(). OnDENYit returns an MCPCallToolResultwithisError: trueand theArcjetDenialResultpayload onstructuredContent. Donât throw. A throw is a raw exception. OmittingisErrorlooks like success.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 })
in UserPromptSubmit. There is no guardCanUseTool and no guardApproval.
Never call createAgentContext inside a Claude callback. Donât 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.
| You have | Use | Needs | Blocks a call? |
|---|---|---|---|
| Any Python callable | guard_action / guard_action_sync | arcjet | Yes |
An authored @tool | guard_tool | arcjet[claude-agent-sdk] | Yes |
| An unwrapped built-in or MCP tool | guard_hooks (PreToolUse) | arcjet[claude-agent-sdk] | Yes |
| Inbound user text | guard_hooks (UserPromptSubmit) | arcjet[claude-agent-sdk] | Yes |
A caller-owned UUID session_id | claude_agent_context | arcjet[claude-agent-sdk] | No |
guard_tool wraps an authored @tool. On DENY, or when Guard
cannot be evaluated and on_guard_error is "deny", the helper
returns JSON of ArcjetDenialResult in content with
is_error: True. The tool handler does not run. Python does not
forward structuredContent. The model reads JSON from content.
Donât raise. A raise is a raw exception. Omitting is_error looks
like success.
guard_hooks screens inbound text on UserPromptSubmit and denies
unwrapped built-in and MCP tools on PreToolUse. There is no
inbound helper. Screen prompt injection on the hooks inbound path.
claude_agent_context reads a caller-owned UUID session_id from
hook input, then the session_id= fallback. It never mints an ID.
It never reads trace_id. If the caller did not pass a UUID
session_id, the call is uncorrelated.
There is no guard_inbound. There is no guard_can_use_tool and no
guard_approval. can_use_tool is human-in-the-loop (HITL)
confirmation, not policy.
Donât also wrap these tools with @arcjet/guard/claude-agent-sdk/v0.
Helper options
Section titled âHelper optionsâ| Option | Helpers | Description |
|---|---|---|
action | guardTool, guardHooks | Guard label and capture name. A string, or for hooks a function of { toolName }. |
rules | guardTool, guardHooks | SDK rules, or a function of the tool input or { toolName }. Omit to submit none. |
actor | guardTool, guardHooks | Trusted identity, or a function resolved per call. Take it from authenticated application state, never from a model-produced argument. |
inputs | guardTool, guardHooks | Named values built with policyInput.server.* or policyInput.local.*, or a function resolved per call. |
inbound | guardHooks | Policy for UserPromptSubmit. Uses { prompt } in rules. |
exclude | guardHooks | Tools already wrapped with guardTool. Pass { server, name } for authored MCP tools or a bare string for a built-in such as "Bash". |
sessionId | guardHooks | Fallback correlation ID. Must be a UUID if you also pass it to query(). |
onGuardError | Both | "deny" (default) or "allow". |
guard_tool accepts this contract:
| Option | Required | Description |
|---|---|---|
guard | Yes | Client from launch_arcjet or launch_arcjet_sync. |
tool | Yes | The authored @tool handler to gate. |
action | Yes | Guard label and capture name. Use resource.verb in the past tense, such as email.sent. |
actor | No | Who is acting, or a function of the parsed tool arguments. Take it from authenticated application state, never from a model-produced argument. |
inputs | No | Typed remote-policy inputs, or a function of the parsed tool arguments. |
rules | No | Bound SDK rule inputs, or a function of the parsed tool arguments. Omit to submit none. |
metadata | No | Capture metadata, or a function of the parsed tool arguments. |
session_id | No | Caller-owned UUID. An authored handler has no extra.session_id, so pass the same id you give ClaudeAgentOptions. Never minted. |
correlation_id | No | Caller-owned UUID. Falls back to the enclosing arcjet_sequence. |
on_guard_error | No | "deny" (default) or "allow". A real DENY always blocks. |
guard_hooks accepts this contract:
| Option | Required | Description |
|---|---|---|
guard | No | Client from launch_arcjet or launch_arcjet_sync. Omit to use the client you registered with register_arcjet. |
action | No | Guard label and capture name. A string, or a function of the hook input. Defaults to {tool_name}.invoked when a tool hook is registered. |
actor | No | Who is acting, or a function of the hook input. Take it from authenticated application state, never from a model-produced argument. |
inputs | No | Typed remote-policy inputs, or a function of the hook input. |
rules | No | Bound SDK rule inputs, or a function of the tool arguments. Omit to submit none. |
metadata | No | Capture metadata, or a function of the hook input. |
inbound | No | Policy for UserPromptSubmit. Requires action. rules receives {"prompt": ...}. |
tools | No | Pass True to register the tool hooks without giving them a policy of their own. |
exclude | No | Tools already wrapped with guard_tool. Pass {"server": ..., "name": ...} to match mcp__{server}__{name}, or a bare string for a built-in such as "Bash". |
session_id | No | Caller-owned UUID fallback. Hook session_id is preferred. Never minted. |
correlation_id | No | Caller-owned UUID. Falls back to the enclosing arcjet_sequence. |
on_guard_error | No | "deny" (default) or "allow". A real DENY always blocks. |
guard_hooks needs a tool policy (action, rules, exclude, or
tools=True), an inbound policy, or both. There is no guard_inbound.
The Python helper registers only what you ask for. Giving it inbound alone
registers UserPromptSubmit and leaves tool calls ungated. The JavaScript
guardHooks always registers PreToolUse, UserPromptSubmit, and
PostToolUse, so the same-looking call gates every tool as well.
Screen inbound with UserPromptSubmit
Section titled âScreen inbound with UserPromptSubmitâThere is no inbound helper. Screen prompt injection on the hooks inbound path
in UserPromptSubmit. This is the only place a turn can be declined before the
model sees the prompt.
On DENY, UserPromptSubmit returns a block decision and Claude Code erases
the prompt. The model never sees it.
Helpers fail closed by default. Failing open on inbound is a legitimate choice, because failing closed there stops the agent answering during an outage. Timeout already fail-closes the prompt (Claude Code v2.1.208+).
On DENY, UserPromptSubmit returns { decision: "block", reason }.
import { query } from "@anthropic-ai/claude-agent-sdk";import { randomUUID } from "node:crypto";import { guardHooks } from "@arcjet/guard/claude-agent-sdk/v0";import { detectPromptInjection } from "@arcjet/guard";import { arcjet } from "./arcjet.js";
// `options.sessionId` must be a UUID and can only be created once, so mint one// per conversation and store it. For more information about session IDs, see// the Correlation section.const sessionId = conversationId ?? randomUUID();
for await (const message of query({ prompt: userText, options: { // First turn only. Later turns: `resume: sessionId`. sessionId, hooks: guardHooks(arcjet, { sessionId, inbound: { action: "message.received", rules: ({ prompt }) => [detectPromptInjection()(prompt)], }, }), },})) { void message;}On DENY, UserPromptSubmit returns {"decision": "block"}.
Pass the UUID session_id that you already have. Donât mint one.
If you omit a UUID session_id, the call is uncorrelated.
from claude_agent_sdk import ClaudeAgentOptions, queryfrom arcjet.guard import DetectPromptInjection, launch_arcjetfrom arcjet.guard.claude_agent_sdk import guard_hooks
arcjet = launch_arcjet(key=ARCJET_KEY)inbound = DetectPromptInjection()
options = ClaudeAgentOptions( session_id=conversation_id, hooks=guard_hooks( guard=arcjet, session_id=conversation_id, inbound={ "action": "message.received", "rules": lambda arguments: [inbound(arguments["prompt"])], }, ),)
async for message in query(prompt=user_text, options=options): passGate authored tool handlers
Section titled âGate authored tool handlersâAn authored tool handler is the local side effect this integration can stop. On
DENY the handler does not run and the model receives a denial result.
Scan free-text arguments (a note, reason, or body). An opaque order ID doesnât trip email, phone, card, or IP detection, so donât pass it to the local sensitive information helper. That helper runs the Rampart model on-device.
The model receives an MCP CallToolResult with isError: true and the
ArcjetDenialResult payload on structuredContent. Donât throw. A throw is a
raw exception. Omitting isError looks like success.
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 { rampart } from "@arcjet/sensitive-info-rampart";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({ deny: ["EMAIL", "PHONE_NUMBER", "IP_ADDRESS", "CREDIT_CARD_NUMBER"], backend: rampart(),});
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), ], },);The helper returns JSON of ArcjetDenialResult in content with
is_error: True:
{ "arcjetDenied": true, "reason": "RATE_LIMIT", "message": "Arcjet denied this call (RATE_LIMIT). It may be retried after 30 seconds.", "retryable": true, "retryAfterSeconds": 30}Python does not forward structuredContent. The model reads JSON
from content. Donât raise. A raise is a raw exception. Omitting
is_error looks like success.
from claude_agent_sdk import toolfrom arcjet.guard import ( LocalDetectSensitiveInfo, TokenBucket, launch_arcjet,)from arcjet.guard.claude_agent_sdk import guard_toolfrom arcjet_sensitive_info_rampart import rampart
arcjet = launch_arcjet(key=ARCJET_KEY)lookup_limit = TokenBucket( refill_rate=10, interval_seconds=60, max_tokens=10, bucket="lookups",)detect_pii = LocalDetectSensitiveInfo( deny=["EMAIL", "PHONE_NUMBER", "IP_ADDRESS", "CREDIT_CARD_NUMBER"], backend=rampart(),)
@tool("lookup_order", "Look up an order by ID", {"order_id": str, "note": str})async def lookup_order(args: dict) -> dict: return { "content": [ { "type": "text", "text": f"{args['order_id']}: shipped ({args['note']})", } ] }
guarded_lookup = guard_tool( guard=arcjet, tool=lookup_order, action="order.looked-up", session_id=conversation_id, rules=lambda arguments: [ lookup_limit(key="orders", requested=1), detect_pii(arguments["note"]), ],)Deny unwrapped tools with PreToolUse
Section titled âDeny unwrapped tools with PreToolUseâBuilt-ins (Bash, Write, and similar) and MCP tools not passed through the
authored-tool wrapper 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.
permissionDecision: "ask" is human-in-the-loop confirmation, not deny.
Donât apply the authored-tool wrapper and PreToolUse to the same tool. That
double-calls the guard.
PreToolUse fires for every tool, and its input carries only a name, never
the Arcjet marker the authored-tool wrapper applies â so list your wrapped
tools in exclude or each one is guarded twice per invocation, costing two
round trips and two quota units.
Entries match the reported name exactly. An authored tool arrives as
mcp__<server>__<tool>, so pass a server and name pair and the qualified name
is built for you; a bare string matches as-is, which is what you want for a
built-in such as "Bash". A bare authored name deliberately does not match
every serverâs tool of that name: two servers can expose the same name with
only one of them wrapped, and a loose match would drop the gate on the
unprotected one. Excluding a tool stops the gate, not the PostToolUse
capture.
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 })], // Tools already wrapped with `guardTool` guard themselves. exclude: [{ server: "support", name: "lookup_order" }], inbound: { action: "message.received", rules: ({ prompt }) => [detectPromptInjection()(prompt)], },});Pass hooks to query({ options.hooks }). Use this for tools you did not
pass through guardTool.
from arcjet.guard import DetectPromptInjection, TokenBucketfrom arcjet.guard.claude_agent_sdk import guard_hooks
mcp_limit = TokenBucket( refill_rate=20, interval_seconds=60, max_tokens=20, bucket="mcp-access",)
hooks = guard_hooks( guard=arcjet, session_id=conversation_id, action=lambda hook: f"{hook['tool_name']}.invoked", rules=[mcp_limit(key="mcp", requested=1)], exclude=[{"server": "support", "name": "lookup_order"}], inbound={ "action": "message.received", "rules": lambda arguments: [ DetectPromptInjection()(arguments["prompt"]) ], },)Pass hooks to ClaudeAgentOptions. Use this for tools you did
not pass through guard_tool.
Tool permission callbacks are not a policy gate
Section titled âTool permission callbacks are not a policy gateâallowedTools, allow rules, and bypassPermissions / acceptEdits skip the
Claude permission callback, so the callback looks like a gate that the runtime
can step around. It also asks a person rather than evaluating a policy. For
more information, see
Human approval is not a policy gate.
There is no Arcjet wrapper for the permission callback, and there is no approval
helper. Donât put Arcjet policy on a permission callback. Gate authored tools
with the authored-tool wrapper, and unwrapped ones with PreToolUse.
There is no guardCanUseTool. Donât put Arcjet policy on canUseTool. Use
guardTool for authored tools or PreToolUse for unwrapped ones.
can_use_tool pauses the run for human-in-the-loop confirmation.
allowed_tools, allow rules, and bypassPermissions / acceptEdits
can skip that callback.
There is no guard_can_use_tool. Donât wrap can_use_tool as Guard.
permissionDecision: "ask" is HITL, not deny. Use guard_tool on
authored @tool handlers you own, or PreToolUse through
guard_hooks for unwrapped tools.
Fail-closed default
Section titled âFail-closed defaultâClaude Agent SDK helpers fail closed by default. If Guard cannot be evaluated,
the tool does not run and inbound UserPromptSubmit blocks the prompt. Failing
open 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+).
A DENY conclusion always blocks, whatever the fail behavior is set to.
The core guard call still fails open. It returns ALLOW and reports that it
failed open. The wrappers that sit around an effect fail closed.
guardTool and guardHooks default to onGuardError: "deny". Set
onGuardError: "allow" only when you can accept running the action without a
complete security decision.
guard_tool and guard_hooks default to
on_guard_error="deny". If Guard cannot be evaluated, guard_tool
returns JSON of ArcjetDenialResult in content with
is_error: True and the tool handler does not run.
Set on_guard_error="allow" only when you can accept running the
action without a complete security decision. The helpers reject any
other value.
has_failed_open() returns True when the core guard() call failed open.
For more information about fail-open versus fail-closed behavior, see Availability and fail behavior.
Correlation
Section titled âCorrelationâTwo Claude Agent SDK constraints decide what you can pass, and neither is Arcjetâs:
- A session ID must be a UUID. Anything else exits the CLI with an invalid session ID error.
- A session ID can only be created once. Passing the same ID to a second query exits with an already-in-use error. Continue an existing conversation by resuming it.
So mint one UUID per conversation and resume it on later turns. That is also
what keeps a multi-turn conversation on a single Sequence: because the context
helper prefers the hookâs session_id, a fresh UUID per turn splits
correlation silently instead of erroring.
The context helper never mints an ID. If it receives no valid 1-256
printable-ASCII string, the call is uncorrelated rather than joined to a
generated ID. Subagent agent_id is metadata only.
claudeAgentContext reads hook session_id first, then options.sessionId. It
never calls createAgentContext.
import { randomUUID } from "node:crypto";
// Store this with the conversation. Don't generate one per turn.const sessionId = conversationId ?? randomUUID();
async function turn(userText: string, firstTurn: boolean) { for await (const message of query({ prompt: userText, options: { ...(firstTurn ? { sessionId } : { resume: sessionId }), hooks: guardHooks(arcjet, { sessionId }), }, })) { void message; }}Pass sessionId to guardHooks either way: a resumed turn reports the same id
on hook input, and the policy value is the fallback for when it does not. A
single query() with a streaming-input prompt is the other supported multi-turn
shape, and needs no resume.
query() has no conversation or session ID unless you pass one.
Pass the UUID session_id that you already have. Donât mint an
ID. claude_agent_context reads hook session_id, then the
session_id= fallback. An authored handler has no
extra.session_id, so pass session_id= on guard_tool.
trace_id is never read.
options = ClaudeAgentOptions( session_id=conversation_id, hooks=guard_hooks( guard=arcjet, session_id=conversation_id, inbound={ "action": "message.received", "rules": lambda arguments: [inbound(arguments["prompt"])], }, ),)Derive the ID from a session that the caller already has. A generated ID still joins this runâs events. It does not match an ID that you already search for.
What not to use
Section titled âWhat not to useâ- There is no inbound helper. Screen prompt injection on the hooks inbound path
in
UserPromptSubmit. - There is no approval helper. A tool permission callback is confirmation, not policy.
- Donât signal a denial by throwing or raising. Return the tool result with the error flag set.
- Donât apply the authored-tool wrapper and
PreToolUseto the same tool.
- There is no
guardInbound, noguardCanUseTool, and noguardApproval. - Donât call
createAgentContextinside a Claude callback. - Donât also wrap these tools with
@arcjet/guard/vercel-ai/v7. - Donât throw from
guardToolto signal a denial. Return theCallToolResultwithisError: true. - Donât import
@arcjet/guard/claude-agent-sdk. The path is@arcjet/guard/claude-agent-sdk/v0.
- There is no
guard_inbound, noguard_can_use_tool, and noguard_approval. - Donât raise from
guard_toolto signal a denial. Return JSON in content withis_error: True. - Donât depend on
structuredContent. Python does not forward it. - Donât mint a session ID. If the caller did not pass a UUID
session_id, leave the call uncorrelated. - Donât wrap these tools with
@arcjet/guard/claude-agent-sdk/v0.
Send values to a policy
Section titled âSend values to a policyâA policy reads the actor and the typed inputs the call carries. Both adapters map them.
guardTool takes actor and inputs, each as a value or a function
resolved per call. A resolver receives the parsed tool input, then the agent
context.
const sendEmail = guardTool(arcjet, sendEmailTool, { action: "email.sent", actor: currentUser.id, inputs: ({ recipient, body }) => ({ recipient: policyInput.server.string(recipient), allowed_recipients: policyInput.server.stringList( currentUser.allowedRecipients, ), body: policyInput.local.string(body), }),});Build every input explicitly with policyInput.server.* or
policyInput.local.*. Plain values are rejected, and the adapter never
discovers arguments for you.
guardHooks takes the same two options. Its tool resolvers receive
{ toolName, input } and its inbound resolvers receive { prompt }, each
followed by the hook input.
guard_tool takes actor and inputs, each as a value or a callable of
the callâs arguments.
guarded_send_email = guard_tool( guard=arcjet, tool=send_email_tool, action="email.sent", actor=current_user.id, inputs=lambda arguments: { "recipient": server_input.string(arguments["recipient"]), "allowed_recipients": server_input.string_list( current_user.allowed_recipients ), "body": local_input.string(arguments["body"]), },)The policy these calls feed declares recipient as a SERVER string,
allowed_recipients as a SERVER string list, and body as a LOCAL string,
then denies a recipient that isnât on the list:
package arcjet.guard
import rego.v1
deny contains "external-recipient" if { not input.values.recipient in input.values.allowed_recipients}Map only what a policy needs. Take the actor and any allow list from authenticated application state, never from the modelâs arguments: a policy can be conditioned on the actor, so a model that controls it can leave its own policy scope.
For the names, kinds, and exposures a policy declares, see Policy contract. For more worked policies, see Policy examples.
Common patterns
Section titled âCommon patternsâ- Prompt injection before the model sees the prompt: the hooks helper with an inbound policy that calls the prompt injection rule.
- Rate limit plus PII on an authored tool: the authored-tool wrapper with a token bucket and the local sensitive information rule on a free-text note.
- Built-ins and MCP tools:
PreToolUsethrough the hooks helper, with every authored-tool wrapper listed inexclude.
Key a rate limit bucket on a trusted identifier you own. Donât key it on free-text user input.
Related
Section titled âRelatedâ- Framework integrations
- Claude Managed Agents agent guard
- Vercel AI SDK agent guard
- LangChain agent guard
- CrewAI agent guard
- Mastra agent guard
- Vercel Eve agent guard
- LangGraph agent guard
- Python Guard SDK reference
- Agent guards
- JavaScript example: examples/claude-agent
- Python example: examples/fastapi-claude-agent-sdk-guard
- Python adapter:
0fa5f546