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 on a local ML model backend.
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 { 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"],});
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_tool
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"],)
@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