Skip to content

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.

Pick the page that matches the object you hold when the effect runs:

FrameworkImport pathStart here
Vercel AI SDK v7@arcjet/guard/vercel-ai/v7Vercel AI SDK agent guard
LangChain (Python)arcjet.guard.langchainLangChain agent guard
LangChain JS@arcjet/guard/langchain/v1LangChain JS agent guard
CrewAI (Python)arcjet.guard.crewaiCrewAI agent guard
LangGraph JS@arcjet/guard/langgraph/v1LangGraph agent guard
Genkit JS@arcjet/guard/genkit/v1Genkit agent guard
OpenAI Agents@arcjet/guard/openai-agents/v0OpenAI Agents agent guard
OpenAI Agents (Python)arcjet.guard.openai_agentsOpenAI Agents Python agent guard
Strands Agents@arcjet/guard/strands-agents/v1Strands Agents agent guard
Vercel Eve@arcjet/guard/vercel-eve/v0Vercel Eve agent guard
Mastra@arcjet/guard/mastra/v1Mastra agent guard
Claude Agent SDK@arcjet/guard/claude-agent-sdk/v0Claude Agent SDK agent guard

Each JavaScript path is versioned. Unversioned aliases such as @arcjet/guard/vercel-ai do not resolve.

Don’t mix adapters. Don’t wrap a Mastra createTool, Eve defineTool, LangGraph tool(), LangChain JS createAgent tool, OpenAI Agents tool(), Python OpenAI Agents function_tool, Claude Agent SDK tool(), Genkit ai.defineTool, Strands Agents tool(), or official CrewAI @tool with @arcjet/guard/vercel-ai/v7. The wrapper throws or misses the real execute path. Don’t use an npm CrewAI port with arcjet.guard.crewai.

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 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_sync in core arcjet.guard. No extra.
  • A LangChain BaseTool you 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.

LangChain JS createAgent agents call authored tools through createMiddleware({ wrapToolCall }). There is no guardInbound. Screen inbound text with a direct guard() call before invoke or stream. humanInTheLoopMiddleware is human HITL confirmation, not policy. Use guardTool() on authored tool() handlers and guardMiddleware() as createMiddleware({ wrapToolCall }). Don’t also wrap these tools with @arcjet/guard/langgraph/v1 or @arcjet/guard/vercel-ai/v7.

This is not the Python LangChain agent guard. It is not LangGraph StateGraph / ToolNode.

guardTool returns a plain { arcjetDenied: true, … }. guardMiddleware wrapToolCall returns a real ToolMessage with JSON content and default status. Don’t throw. Policy sits on wrapToolCall only. For more information about inbound screening, authored tools, and agent-wide denials, see the LangChain JS agent guard.

Official CrewAI crews call authored tools during a kickoff. Register register_arcjet_hooks for process-wide PRE_TOOL_CALL only. POST_TOOL_CALL is not registered. On DENY or fail-closed unavailability the helper raises HookAborted(reason=..., source="arcjet"). The agent always sees Tool execution blocked by hook. Tool: {name}. guard_tool wraps a standalone BaseTool you call yourself and is the only path that raises Arcjet errors. There is no guard_crew. human_input is HITL, not policy. This is not an npm CrewAI port.

There is no arcjet[crewai] extra. Install official crewai>=1.15.3,<2 yourself. Until this module is on PyPI, pin arcjet to b1253640. Published arcjet 0.9.0 does not include it.

For more information about install, helpers, fail-closed behavior, and HookAborted, see the CrewAI agent guard.

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 JS createAgent / wrapToolCall. For that adapter, see the LangChain JS agent guard.

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 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 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.

OpenAI Agents Python text Agent workflows (Runner.run + authored function_tool) have no inbound helper. Screen user text with a direct guard() call before Runner.run. needs_approval and hosted require_approval are human HITL confirmation, not policy. Use guard_tool to attach FunctionTool.tool_input_guardrails. On DENY the helper calls reject_content(...) with JSON of ArcjetDenialResult. Don’t raise from the guardrail. A raise becomes a tripwire halt, or default_tool_error_function swallows it. Hosted tools, handoffs, and Agent.as_tool() are not deny points.

The arcjet[openai-agents] extra depends on openai-agents>=0.19.0,<1.

For more information about inbound screening, authored tools, and reject_content denials, see the OpenAI Agents Python agent guard.

Strands Agents JS Agent workflows (invoke() + authored tool()) have no guardInbound. Screen user text with a direct guard() call before invoke or stream. event.interrupt() is human HITL confirmation, not policy. Use guardTool() on authored tool({ callback }) handlers and guardHooks() as a Plugin on new Agent({ plugins }). On DENY, guardHooks sets event.cancel to a JSON string of ArcjetDenialResult. Don’t set BeforeToolsEvent.cancel. Don’t also wrap these tools with @arcjet/guard/langgraph/v1 or @arcjet/guard/vercel-ai/v7.

guardTool returns { arcjetDenied: true, … } as the tool result. Don’t throw. Don’t call event.interrupt(). For more information about inbound screening, authored tools, and BeforeToolCallEvent denials, see the Strands Agents agent guard.

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 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 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.

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:

AdapterIdiomatic envelopeWhy not the others
AI SDK / MastraReturn { arcjetDenied: true, … } as the tool resultA throw becomes a generic tool error and drops the fields
OpenAI AgentsReturn { arcjetDenied: true, … } from invokeA throw hits errorFunction or ToolCallError and can kill the run
OpenAI Agents (Python)reject_content(...) with JSON of ArcjetDenialResultA raise becomes a tripwire halt, or default_tool_error_function swallows it
Strands AgentsguardTool returns { arcjetDenied: true, … }. guardHooks sets event.cancel to a JSON string of that objectA throw drops the fields. event.interrupt() is HITL, not policy
LangGraphReturn { arcjetDenied: true, … }. ToolNode wraps a ToolMessage with status: "success"Fabricating a ToolMessage to force status: "error" crashes the graph reducer
LangChain JSguardTool returns { arcjetDenied: true, … }. guardMiddleware returns a real ToolMessage (JSON content, default status)A throw drops the fields. wrapToolCall cannot return a bare object. Don’t set status: "error"
GenkitReturn { arcjetDenied: true, … } as completed toolResponse.outputA throw / interrupt() is HITL and the wrong envelope
Claude Agent SDKMCP CallToolResult with isError: true and the payload on structuredContentA throw is a raw exception. Omitting isError looks like success
Vercel EveThrow 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.

The wrappers default to fail closed when Guard is unavailable:

  • Vercel AI does not execute the tool and returns a retryable ArcjetDenialResult to the model with reason ERROR. A throw from guardTool becomes a generic tool error and drops the fields.
  • LangChain guard_tool raises ArcjetToolUnavailableError. A policy denial raises ArcjetToolDeniedError, or follows the wrapped tool’s handle_tool_error behavior when configured. guard_action and ArcjetMiddleware raise ArcjetUnavailableError / ArcjetDeniedError.
  • CrewAI register_arcjet_hooks defaults to on_guard_error="deny". On DENY or unavailability, PRE_TOOL_CALL raises HookAborted(reason=..., source="arcjet") so the tool does not run. The agent always sees Tool execution blocked by hook. Tool: {name}. CrewAI swallows any other exception. POST_TOOL_CALL is not registered. guard_tool raises ArcjetDeniedError / ArcjetUnavailableError.
  • Vercel Eve helpers default to onGuardError: "deny". guardTool throws ArcjetDeniedError. Pass onDeny: "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". guardTool returns { 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". guardTool returns an MCP CallToolResult with isError: true and the payload on structuredContent. On inbound UserPromptSubmit, "allow" is a legitimate choice because failing closed stops the agent answering.
  • LangGraph helpers default to onGuardError: "deny". guardTool returns the plain payload. ToolNode wraps a ToolMessage whose status is success. Direct guard() before invoke fails open. Check decision.hasFailedOpen() when that call site must fail closed.
  • LangChain JS helpers default to onGuardError: "deny". guardTool returns a plain { arcjetDenied: true, … }. guardMiddleware wrapToolCall returns a real ToolMessage with JSON content and default status. Don’t throw. Policy sits on wrapToolCall only. Direct guard() before invoke / stream fails open. Check decision.hasFailedOpen() when that call site must fail closed.
  • Genkit helpers default to onGuardError: "deny". guardTool returns { arcjetDenied: true, … } as completed toolResponse.output. Don’t throw. Don’t call interrupt(). Direct guard() before generate() / send() fails open. Check decision.hasFailedOpen() when that call site must fail closed.
  • OpenAI Agents helpers default to onGuardError: "deny". guardTool returns a plain ArcjetDenialResult and does not throw. A throw hits ToolCallError and drops the fields. The runner stringifies the payload onto a function_call_result with status: "completed". Direct guard() before run() fails open. Check decision.hasFailedOpen() when that call site must fail closed.
  • OpenAI Agents Python guard_tool defaults to on_guard_error="deny". The policy gate is FunctionTool.tool_input_guardrails plus reject_content. The reject payload is JSON of ArcjetDenialResult. Don’t raise from the guardrail. Direct guard() before Runner.run fails open. Check decision.has_failed_open() when that call site must fail closed.
  • Strands Agents helpers default to onGuardError: "deny". guardTool returns a plain { arcjetDenied: true, … }. guardHooks is a Plugin that sets event.cancel to a JSON string of that object. Don’t throw. Don’t call event.interrupt(). Don’t set BeforeToolsEvent.cancel. Policy sits on BeforeToolCallEvent.cancel only. Direct guard() before invoke / stream fails open. Check decision.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.