Agent guard framework integrations
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.AI frameworks normally pass a model’s generated arguments directly to a tool’s
execute or invoke function. Arcjet’s framework integrations add a security
checkpoint between those two steps, so a remote policy or SDK rule evaluates
every tool attempt before the tool can create a side effect.
Use an integration when you want to protect tools without writing decision
handling around every function. The wrapper preserves the framework’s normal
tool definition and result flow, selects policy with a stable action, and maps
the actor and relevant tool arguments into policy inputs.
Use protect() on HTTP routes. Use
the helpers on each framework page for agent tools and other non-HTTP actions.
You choose the context that each policy receives. Map only the tool arguments that the policy needs, and take the actor from your authenticated application context. This keeps the policy contract explicit and avoids sending unrelated data.
Choose an adapter
Section titled “Choose an adapter”Pick the page that matches the object you hold when the effect runs:
| Framework | Import path | Start here |
|---|---|---|
| Vercel AI SDK v7 | @arcjet/guard/vercel-ai/v7 | Vercel AI SDK agent guard |
| LangChain (Python) | arcjet.guard.langchain | LangChain agent guard |
| LangGraph JS | @arcjet/guard/langgraph/v1 | LangGraph agent guard |
| Genkit JS | @arcjet/guard/genkit/v1 | Genkit agent guard |
| OpenAI Agents | @arcjet/guard/openai-agents/v0 | OpenAI Agents agent guard |
| Vercel Eve | @arcjet/guard/vercel-eve/v0 | Vercel Eve agent guard |
| Mastra | @arcjet/guard/mastra/v1 | Mastra agent guard |
| Claude Agent SDK | @arcjet/guard/claude-agent-sdk/v0 | Claude Agent SDK agent guard |
Each JavaScript path is versioned. Unversioned aliases such as
@arcjet/guard/vercel-ai do not resolve.
Don’t mix adapters. A Mastra createTool, Eve defineTool, LangGraph
tool(), OpenAI Agents tool(), Claude Agent SDK tool(), or Genkit
ai.defineTool is not an AI SDK tool(). Wrapping one of those with
@arcjet/guard/vercel-ai/v7 throws or misses the real execute path.
Vercel AI SDK
Section titled “Vercel AI SDK”Wrap authored tool({ execute }) handlers with guardTool. Thread
createAgentContext() through aiToolsContext on generateText /
streamText. There is no guardInbound; screen user text with a direct
guard() call before the model runs.
import { launchArcjet, policyInput } from "@arcjet/guard";import { aiToolsContext, createAgentContext, guardTool,} from "@arcjet/guard/vercel-ai/v7";import { generateText, tool } from "ai";import { z } from "zod";
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
export async function runAgent(user: { id: string }, prompt: string) { const sendEmail = guardTool( arcjet, tool({ description: "Send an email", inputSchema: z.object({ to: z.string(), body: z.string() }), execute: ({ to, body }) => emailProvider.send({ to, body }), }), { action: "email.sent", actor: user.id, inputs: ({ to, body }) => ({ recipient: policyInput.server.string(to), body: policyInput.local.string(body), }), }, );
const tools = { sendEmail }; const context = createAgentContext({ correlationId: user.id });
return generateText({ model: "openai/gpt-4o-mini", prompt, tools, toolsContext: aiToolsContext(context, tools), });}On a policy denial the tool does not execute and the model receives an
ArcjetDenialResult. Don’t throw from guardTool. guardAction throws so
application code can catch. For more information about guardAction,
capture, and inbound screening, see the
Vercel AI SDK agent guard.
LangChain
Section titled “LangChain”LangChain tools and create_agent agents call authored tools. The Python SDK
sits at those boundaries. This is not the JavaScript LangGraph Graph API
adapter.
Pick the surface that matches what you hold when the effect runs:
- Any Python callable:
guard_action/guard_action_syncin corearcjet.guard. No extra. - A LangChain
BaseToolyou call yourself:guard_tool(arcjet[langchain],langchain-core>=1.2.5,<2). - An agent from
create_agent:ArcjetMiddleware+ToolPolicy(arcjet[langchain-agents],langchain>=1.3,<2,langgraph>=1.2,<2). - Observe only:
ArcjetCaptureHandler/ArcjetAsyncCaptureHandler. These cannot deny a call. LangChain ignores callback return values.
For more information about install, helpers, fail-closed behavior, and correlation, see the LangChain agent guard.
LangGraph JS
Section titled “LangGraph JS”LangGraph Graph API agents (StateGraph + ToolNode) call authored tools and
ToolNode / MCP tools. There is no guardInbound. Screen inbound text with a
direct guard() call before graph.invoke, or in the first graph node.
interrupt() is human-in-the-loop (HITL) confirmation, not policy. Use
guardTool() for authored tool() / StructuredTool handlers and
guardToolNode() for unwrapped and MCP tools that execute inside ToolNode.
This is not the Python
LangChain agent guard. It is
not LangChain createAgent / wrapToolCall.
guardTool returns the plain payload. Don’t fabricate a ToolMessage to
force status: "error". For more information about inbound screening,
authored tools, and ToolNode denials, see the
LangGraph agent guard.
Genkit
Section titled “Genkit”Genkit JS flows (ai.generate() / chat.send() plus authored
ai.defineTool) have no guardInbound. Screen user text with a direct
guard() call before generate() or send(). Middleware model is not
Guard. interrupt() / defineInterrupt / @genkit-ai/middleware
toolApproval / restartTool are human HITL confirmation, not policy.
There is no guardApproval. Use guardTool() on the returned
ToolAction and guardMiddleware() as a { name, instantiate }
generateMiddleware tool hook. Don’t also wrap these tools with
@arcjet/guard/vercel-ai/v7. This is not Go or Python Genkit.
guardTool returns { arcjetDenied: true, … } as completed
toolResponse.output. Don’t throw. Don’t call interrupt(). For more
information about inbound screening, authored tools, and
generate()-wide denials, see the
Genkit agent guard.
OpenAI Agents
Section titled “OpenAI Agents”OpenAI Agents text Agent workflows (run() + authored tool()) have no
guardInbound. Screen user text with a direct guard() call before run().
needsApproval and hosted requireApproval are human HITL confirmation, not
policy. Use guardTool() to wrap FunctionTool.invoke after
tool({ execute }). Hosted tools, handoffs, and Runner agent_tool_start are
not deny points.
This is not Realtime, Sandbox, hosted tools, MCP, agent.asTool(), or
computer/shell.
guardTool returns { arcjetDenied: true, … } from invoke. Don’t throw.
For more information about inbound screening, authored tools, and execute
denials, see the
OpenAI Agents agent guard.
Vercel Eve
Section titled “Vercel Eve”Eve agents can have zero authored tools. OpenAPI and MCP connections have no
local execute, so the enforcement point is guardApproval() on the
connection. Screen inbound channel text with guardInbound() before the agent
starts. Hooks are observe-only (handlers return void).
guardTool throws ArcjetDeniedError. Pass onDeny: "result" to return
the payload instead. For more information about inbound screening, authored
tools, connection approvals, and lifecycle hooks, see the
Vercel Eve agent guard.
Mastra
Section titled “Mastra”Mastra has no guardInbound (channels already hit processInput) and no
guardApproval (requireApproval is a human-in-the-loop control). Screen
inbound text with guardProcessor. Gate authored tools with guardTool. Gate
MCP or workspace tools you did not wrap with guardHooks (beforeToolCall can
deny).
guardTool returns { arcjetDenied: true, … } as the tool result. Don’t
throw. For more information about processors, tools, and hooks, see the
Mastra agent guard.
Claude Agent SDK
Section titled “Claude Agent SDK”Claude Agent SDK agents call authored tool() handlers, built-in tools, and
MCP tools. There is no guardInbound. Screen inbound prompts with
guardHooks({ inbound }) on UserPromptSubmit. canUseTool is not a policy
gate. Use guardTool() for authored tools and PreToolUse for unwrapped
built-ins and MCP tools.
guardTool returns an MCP CallToolResult with isError: true and the
payload on structuredContent. Don’t throw. For more information about
inbound screening, authored tools, and PreToolUse denials, see the
Claude Agent SDK agent guard.
Denial responses
Section titled “Denial responses”Every JavaScript adapter uses one payload, ArcjetDenialResult. The fields,
wording, and retry-after rules are the same, so a model sees one shape. The
envelope is per-framework, because each SDK reports a tool that did not run
in a different way:
| Adapter | Idiomatic envelope | Why not the others |
|---|---|---|
| AI SDK / Mastra | Return { arcjetDenied: true, … } as the tool result | A throw becomes a generic tool error and drops the fields |
| OpenAI Agents | Return { arcjetDenied: true, … } from invoke | A throw hits errorFunction or ToolCallError and can kill the run |
| LangGraph | Return { arcjetDenied: true, … }. ToolNode wraps a ToolMessage with status: "success" | Fabricating a ToolMessage to force status: "error" crashes the graph reducer |
| Genkit | Return { arcjetDenied: true, … } as completed toolResponse.output | A throw / interrupt() is HITL and the wrong envelope |
| Claude Agent SDK | MCP CallToolResult with isError: true and the payload on structuredContent | A throw is a raw exception. Omitting isError looks like success |
| Vercel Eve | Throw ArcjetDeniedError. Opt in to a returned payload with onDeny: "result" | Eve projects a throw as a failed action.result. A silent return can violate outputSchema |
const result: ArcjetDenialResult = { arcjetDenied: true, reason: "RATE_LIMIT", message: "Arcjet denied this call (RATE_LIMIT). It may be retried after 30 seconds.", retryable: true, retryAfterSeconds: 30,};You can import ArcjetDenialResult from @arcjet/guard/vercel-ai/v7 and the
other JavaScript adapter namespaces.
guardTool must produce an envelope the model can inspect. guardAction
throws so application code can catch. These cannot share one handler.
Only rate-limit denials set retryable: true and include retryAfterSeconds.
When Guard is unavailable and onGuardError is "deny", the payload uses
reason: "ERROR", retryable: true, and retryAfterSeconds: 5.
Denials and availability
Section titled “Denials and availability”The wrappers default to fail closed when Guard is unavailable:
- Vercel AI does not execute the tool and returns a retryable
ArcjetDenialResultto the model with reasonERROR. A throw fromguardToolbecomes a generic tool error and drops the fields. - LangChain
guard_toolraisesArcjetToolUnavailableError. A policy denial raisesArcjetToolDeniedError, or follows the wrapped tool’shandle_tool_errorbehavior when configured.guard_actionandArcjetMiddlewareraiseArcjetUnavailableError/ArcjetDeniedError. - Vercel Eve helpers default to
onGuardError: "deny".guardToolthrowsArcjetDeniedError. PassonDeny: "result"to return the payload instead. On a channel,"allow"is a legitimate choice because failing closed stops the agent answering. - Mastra helpers default to
onGuardError: "deny".guardToolreturns{ arcjetDenied: true, … }as the tool result. A throw becomes a generic tool error and drops the fields."allow"is a legitimate choice on the inbound processor because failing closed stops the agent answering. - Claude Agent SDK helpers default to
onGuardError: "deny".guardToolreturns an MCPCallToolResultwithisError: trueand the payload onstructuredContent. On inboundUserPromptSubmit,"allow"is a legitimate choice because failing closed stops the agent answering. - LangGraph helpers default to
onGuardError: "deny".guardToolreturns the plain payload.ToolNodewraps aToolMessagewhosestatusissuccess. Directguard()beforeinvokefails open. Checkdecision.hasFailedOpen()when that call site must fail closed. - Genkit helpers default to
onGuardError: "deny".guardToolreturns{ arcjetDenied: true, … }as completedtoolResponse.output. Don’t throw. Don’t callinterrupt(). Directguard()beforegenerate()/send()fails open. Checkdecision.hasFailedOpen()when that call site must fail closed. - OpenAI Agents helpers default to
onGuardError: "deny".guardToolreturns a plainArcjetDenialResultand does not throw. A throw hitsToolCallErrorand drops the fields. The runner stringifies the payload onto afunction_call_resultwithstatus: "completed". Directguard()beforerun()fails open. Checkdecision.hasFailedOpen()when that call site must fail closed.
Set onGuardError: "allow" in JavaScript or on_guard_error="allow" in Python
only when executing without a complete security decision is acceptable.
For the direct client and wrapper differences, see Availability and fail behavior.