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, and selects policy with a stable action.
Where the adapter supports it, the wrapper also maps the actor and the 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.
Where an adapter maps policy inputs, you choose what the 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.
How the SDKs differ
Section titled “How the SDKs differ”Every Arcjet SDK offers the same two things: a direct guard() call that you
place yourself, and helpers that place the same check around a framework’s tool
for you. What changes between languages is how a helper is packaged, how it
receives the client, and how it reports a denial. One rule accounts for most of
the difference:
A helper that answers a model returns a value. A helper that answers your application signals an error.
A denied tool call has to reach the model as something it can read and respond
to, so guardTool and its equivalents return a denial payload rather than
throwing. A denied application call has to reach code that can handle it, so
guardAction throws in JavaScript and raises in Python.
Direct Guard calls
Section titled “Direct Guard calls”The direct client is available in JavaScript, Python, and Go.
| JavaScript | Python | Go | |
|---|---|---|---|
| Package | @arcjet/guard | arcjet.guard | github.com/arcjet/arcjet-go |
| Create a client | launchArcjet() | launch_arcjet() and launch_arcjet_sync() | arcjet.NewGuardClient() |
| Evaluate a policy | await arcjet.guard({ label, rules }) | await arcjet.guard(label=, rules=) | guard.Guard(ctx, arcjet.GuardRequest{Label: …}) |
Reads ARCJET_KEY | No | No | Yes, when Key is empty |
| Process-wide registry | registerArcjet(), then free guard() | register_arcjet(), then free guard() or guard_sync() | None. Pass the client |
| Rule mode | Defaults to LIVE | Defaults to LIVE | Mode is required. An empty Mode returns ErrInvalidMode |
| Incomplete check | Fails open. Check decision.hasFailedOpen() | Fails open. Check decision.has_failed_open() | Fails open. Check decision.HasFailedOpen() |
Framework helpers
Section titled “Framework helpers”Framework helpers are available for JavaScript and Python. Go has the direct client and capture.
| JavaScript | Python | |
|---|---|---|
| Import path | A versioned subpath per vendor major, such as @arcjet/guard/langchain/v1. Unversioned aliases don’t resolve | A submodule per vendor, such as arcjet.guard.langchain |
| Framework dependency | An optional peer dependency | An extra, such as arcjet[langchain]. CrewAI has no extra |
| Client | The first argument | The guard= keyword. Several helpers accept a registered client instead |
| Denial the model reads | ArcjetDenialResult in that framework’s envelope | The same fields in that framework’s envelope |
| Denial your application catches | guardAction throws ArcjetDeniedError or ArcjetGuardUnavailableError | guard_action raises ArcjetDeniedError or ArcjetUnavailableError |
| Fail-closed default | onGuardError: "deny" | on_guard_error="deny" |
| Blocking variant | None. The client is async | Every entry point has a _sync pair |
For the shape each framework’s denial takes, see Denial responses.
Choose an adapter
Section titled “Choose an adapter”Pick the page that matches the object you hold when the effect runs. Each page covers install, every helper, the denial envelope, and correlation for that framework.
| Framework | Language | Import path | Enforcement point |
|---|---|---|---|
| Vercel AI SDK v7 | JavaScript | @arcjet/guard/vercel-ai/v7 | guardTool on an authored tool({ execute }) |
| LangChain | JavaScript | @arcjet/guard/langchain/v1 | guardTool on an authored tool, guardMiddleware on wrapToolCall |
| LangChain | Python | arcjet.guard.langchain | guard_tool on a BaseTool, ArcjetMiddleware on create_agent |
| CrewAI | Python | arcjet.guard.crewai | register_arcjet_hooks on PRE_TOOL_CALL |
| LangGraph | JavaScript | @arcjet/guard/langgraph/v1 | guardTool on an authored tool, guardToolNode on ToolNode |
| Genkit | JavaScript | @arcjet/guard/genkit/v1 | guardTool on a ToolAction, guardMiddleware on the tool hook |
| Google ADK | JavaScript | @arcjet/guard/google-adk/v2 | guardPlugin on beforeToolCallback |
| OpenAI Agents | JavaScript | @arcjet/guard/openai-agents/v0 | guardTool on FunctionTool.invoke |
| OpenAI Agents | Python | arcjet.guard.openai_agents | guard_tool on FunctionTool.tool_input_guardrails |
| Strands Agents | JavaScript | @arcjet/guard/strands-agents/v1 | guardTool on an authored tool, guardHooks on BeforeToolCallEvent |
| Strands Agents | Python | arcjet.guard.strands_agents | guard_tool on an authored tool, guard_hooks on cancel_tool |
| TanStack AI | JavaScript | @arcjet/guard/tanstack-ai/v0 | guardMiddleware on onBeforeToolCall |
| Vercel Eve | JavaScript | @arcjet/guard/vercel-eve/v0 | guardApproval on a connection, guardTool on execute, guardInbound on a channel |
| Mastra | JavaScript | @arcjet/guard/mastra/v1 | guardTool on execute, guardHooks on beforeToolCall, guardProcessor on inbound text |
| Claude Agent SDK | JavaScript | @arcjet/guard/claude-agent-sdk/v0 | guardTool on an authored tool, guardHooks on PreToolUse and UserPromptSubmit |
| Claude Agent SDK | Python | arcjet.guard.claude_agent_sdk | guard_tool on an authored @tool, guard_hooks on PreToolUse and UserPromptSubmit |
| Claude Managed Agents | JavaScript | @arcjet/guard/claude-managed-agents/v0 | guardEvents before events.send, guardCustomTool on agent.custom_tool_use |
| Claude Managed Agents | Python | arcjet.guard.claude_managed_agents | guard_events before events.send, guard_custom_tool on agent.custom_tool_use |
Each JavaScript path is versioned. Unversioned aliases such as
@arcjet/guard/vercel-ai do not resolve.
Each adapter wraps the object that its own framework executes, so use the adapter that matches the framework you are calling. Wrapping one framework’s tool with another framework’s adapter either throws or misses the path the framework runs, which leaves the tool unguarded. The Python CrewAI helpers work with official CrewAI rather than an npm port of it.
Human approval is not a policy gate
Section titled “Human approval is not a policy gate”Most agent frameworks offer a way to pause a run so that a person can approve a tool call:
| Framework | Approval control |
|---|---|
| Claude Agent SDK | A permission callback |
| Claude Managed Agents | always_ask with user.tool_confirmation |
| CrewAI | human_input |
| Genkit | toolApproval |
| LangChain, LangGraph | An interrupt |
| Mastra | requireApproval |
| OpenAI Agents | An approval flag |
| Strands Agents | An interrupt |
| TanStack AI | needsApproval |
| Vercel Eve | A connection’s approval field |
None of these is a Guard enforcement point. Each one asks a person instead of
evaluating a policy, and a run that nobody is watching stalls. Arcjet does not
wrap any of them as a gate, and no adapter has a guardApproval helper except
Vercel Eve, where the approval field is the only place the framework lets you
intercept an OpenAPI or MCP connection.
Use a framework’s approval control when a person must decide, and a Guard helper when a policy must decide. The two compose, so an approval pause can sit in front of a guarded tool.
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 (JavaScript) | Return { arcjetDenied: true, … } from invoke | A throw hits errorFunction or ToolCallError and can kill the run |
| OpenAI Agents (Python) | reject_content(...) with JSON of ArcjetDenialResult | A raise becomes a tripwire halt, or default_tool_error_function swallows it |
| Strands Agents (JavaScript) | guardTool returns { arcjetDenied: true, … }. guardHooks sets event.cancel to a JSON string of that object | A throw drops the fields. event.interrupt() is HITL, not policy |
| Strands Agents (Python) | guard_tool returns JSON of ArcjetDenialResult. guard_hooks sets cancel_tool to True or a string | A raise is a raw exception. event.interrupt() is HITL, not policy |
| TanStack AI | onBeforeToolCall skip with ArcjetDenialResult ({ type: "skip", result }). Optional onDeny: "abort" | A throw from execute is swallowed. There is no guardTool. Abort is not the default |
| LangGraph | Return { arcjetDenied: true, … }. ToolNode wraps a ToolMessage with status: "success" | Fabricating a ToolMessage to force status: "error" crashes the graph reducer |
| LangChain (JavaScript) | guardTool 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" |
| Genkit | Return { arcjetDenied: true, … } as completed toolResponse.output | A throw / interrupt() is HITL and the wrong envelope |
| Google ADK | Deny dict from beforeToolCallback. undefined executes | A throw is the wrong envelope. undefined on error executes the tool |
| Claude Agent SDK (JavaScript) | MCP CallToolResult with isError: true and the payload on structuredContent | A throw is a raw exception. Omitting isError looks like success |
| Claude Agent SDK (Python) | JSON in content + is_error: True | Python does not forward structuredContent. A raise is a raw exception. Omitting is_error looks like success |
| Claude Managed Agents | Don’t send user.message on inbound DENY. On custom-tool DENY, don’t execute; return user.custom_tool_result with is_error | Built-in tools already ran under Anthropic. user.tool_confirmation is opt-in HITL, not policy. A throw is a raw exception. is_error is on the events schema |
| 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 direct client and the framework helpers answer an incomplete check differently.
A direct guard() call fails open. It returns ALLOW with an error result, so
hasFailedOpen(), has_failed_open(), or HasFailedOpen() reports true.
Check it at any call site that must fail closed.
Every framework helper fails closed. The tool does not run, and the model
receives the same ArcjetDenialResult it would receive for a policy denial,
carrying reason: "ERROR", retryable: true, and retryAfterSeconds: 5, in
that framework’s envelope. Set onGuardError: "allow" in JavaScript or
on_guard_error="allow" in Python only when running the action without a
complete security decision is acceptable.
Inbound screening is the case where "allow" is often the right choice,
because failing closed on a user’s first message stops the agent answering at
all.
Two helpers report unavailability differently from the preceding table:
- Python LangChain
guard_toolraisesArcjetToolUnavailableError, and a policy denial raisesArcjetToolDeniedError. Both subclass LangChain’sToolException, so a wrapped tool’shandle_tool_errorhandles them when you configure one. - CrewAI
register_arcjet_hooksraisesHookAborted(reason=..., source="arcjet")fromPRE_TOOL_CALLfor both a denial and unavailability, so the agent seesTool execution blocked by hook. Tool: {name}in each case.
For the direct client and wrapper differences, see Availability and fail behavior.