integrate-arcjet-guard-langgraph
Integrate Arcjet security into a LangGraph Graph API agent using @arcjet/guard — wrap tool() / StructuredTool, wrap ToolNode for unwrapped MCP tools, and read thread_id for correlation. Use when asked to add Arcjet to a LangGraph StateGraph / ToolNode agent, rate limit its tools, screen inbound messages, or block prompt injection / PII.
git clone --depth 1 https://github.com/arcjet/arcjet-js /tmp/integrate-arcjet-guard-langgraph && cp -r /tmp/integrate-arcjet-guard-langgraph/arcjet-guard/skills/integrate-arcjet-guard-langgraph ~/.claude/skills/integrate-arcjet-guard-langgraphSKILL.md
# Integrate Arcjet Guard into a LangGraph agent
`@arcjet/guard`'s LangGraph v1 namespace wraps the agent's existing Arcjet
client. It never talks to the Arcjet API itself. Two surfaces, one
decision rule:
- **An authored tool** (`tool()` / `StructuredTool`) → `guardTool()`. DENY
returns a structured `ArcjetDenialResult`. Do not throw.
- **MCP / runtime-discovered / unwrapped tools** → `guardToolNode()`.
Guards the tools a `ToolNode` from `@langchain/langgraph/prebuilt`
executes, in place, so execute still hits Guard. Already-branded tools
are skipped (no double-call).
- **Correlation** → `langgraphAgentContext()` reads
`configurable.thread_id`, then the run id, then `checkpoint_ns`. It never
mints a new id.
This namespace is LangGraph **Graph API** (`StateGraph` + `ToolNode`).
`createReactAgent` is deprecated in LangGraph JS v1 in favor of LangChain
`createAgent` / `wrapToolCall`. Do not build on `createReactAgent`. Do not
use this path for a LangChain `createAgent` app — that is
`@arcjet/guard/langchain/v1`.
## Screen inbound before `invoke` (or at the first graph node)
There is no first-class LangGraph channel for inbound screening, so there
is no `guardInbound`. Put prompt-injection (and other inbound rules) in
the application before `graph.invoke`, or in the graph's first node.
## `interrupt()` is not a policy gate
`interrupt()` / `interrupt_before=["tools"]` is human-in-the-loop, not
policy. Same trap as Mastra `requireApproval` and Claude `canUseTool`.
There is no `guardInterrupt` and no `guardApproval`. Do not wrap them as
Guard.
## `ToolNode` is the deny point for tools; hooks / HITL cannot enforce
Unwrapped and MCP tools run inside `ToolNode`. Graph hooks and HITL
pauses cannot stop `tool.invoke`. Use `guardToolNode` (or `guardTool` for
authored tools you invoke yourself).
`guardToolNode` guards the node's tools **in place** and returns the same
node. `ToolNode`'s constructor captures
`func: (input, config) => this.run(input, config)` and `run` reads
`this.tools`, so guarding a copy would leave the original node running
unguarded tools. This also means a caller still holding the pre-wrap node
cannot bypass Guard.
## Questions to ask the human first
Ask only what you cannot infer from the code; suggest defaults.
1. Which tools are **risky** (external side effects, irreversible, spends
money, sends messages)? Those get `guardTool`. MCP / tools you did not
author get `guardToolNode`.
2. What **limits**? (e.g. "10 lookups/min per order" → `tokenBucket`.)
3. Who is the **user** for metadata — an opaque user/tenant ID (never PII)?
Default: none. Pass it via `metadata` on the policy. `thread_id` is the
correlation id, not the user.
4. Is an Arcjet outage unacceptable? Every helper defaults to
`onGuardError: "deny"`. Ask explicitly about inbound screening before
`invoke`: failing closed there means the graph does not run for the
duration of the outage, so `"allow"` is a routine and legitimate
choice at that one call site.
## The six things readers get wrong
1. **There is no `guardInbound`.** Screen prompt injection before
`graph.invoke` or in the first graph node.
2. **`interrupt()` is not a policy gate.** It is HITL. Use `guardTool` or
`guardToolNode`.
3. **The import path is versioned and there is no alias.**
`@arcjet/guard/langgraph/v1`. `@arcjet/guard/langgraph` does not resolve.
4. **Correlation is read, never minted.** Do not call `createAgentContext`
inside a LangGraph callback — that generates a second id and splits the
Sequence. `langgraphAgentContext` reads `thread_id` / `checkpoint_ns` /
run id and omits `correlationId` when none of those is a valid id.
5. **Do not double-wrap with `@arcjet/guard/vercel-ai/v7`.** LangGraph
tools are LangChain `tool()`, but this namespace brands them. `guardTool`
throws if the tool already carries the Arcjet protection brand.
`guardToolNode` skips already-branded tools so Guard is not double-called.
6. **A denial from `guardTool` is a structured object, not a throw.**
`ToolNode` turns it into a real `ToolMessage`. Because the tool did not
throw, that message's `status` is `success` — the denial is in the
payload (`arcjetDenied: true`). Do not fabricate a `ToolMessage`
yourself to force `status: "error"`: an object that only looks like a
message reaches the graph's message reducer and crashes it. If `onDeny`
throws, the tool still does not run and the model still receives the
default denial.
## Step 1: Install and find the guard client
Install `@arcjet/guard` (required), plus `@langchain/langgraph` and
`@langchain/core` (optional peers, needed for
`@arcjet/guard/langgraph/v1`). Always use the versioned path:
`@arcjet/guard/langgraph/v1` resolves; `@arcjet/guard/langgraph` throws
`ERR_PACKAGE_PATH_NOT_EXPORTED`.
```sh
npm install @arcjet/guard @langchain/langgraph @langchain/core
```
If the agent has no guard client yet, launch one **once at module scope**:
```ts
import { launchArcjet } from "@arcjet/guard";
export const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
```
## Step 2: Gate authored tools
```ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { guardTool } from "@arcjet/guard/langgraph/v1";
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)`.
// Scan free-text args (a note, reason, body). An opaque `orderId` will
// not trip EMAIL / phone / card / IP, so do not pass it here.
const detectPii = localDetectSensitiveInfo();
export const lookupOrder = guardTool(
arcjet,
tool(async ({ orderId, note }) => ({ orderId, note, status: "shipped" }), {
name: "lookup_order",
description: "Look up an order by ID",
schema: z.object({
orderIdIntegrate Arcjet security into a Vercel AI SDK (v7) application using @arcjet/guard — wrap agent tools with guard checks, enforce rules on risky app actions, and emit audit events joined by one correlation ID. Use when asked to add Arcjet to an AI SDK app, protect or rate limit agent tool calls, guard AI agent actions, or audit what an agent did.
Integrate Arcjet security into a Claude Agent SDK agent using @arcjet/guard — wrap tool() handlers, screen inbound prompts with UserPromptSubmit, and deny unwrapped built-in/MCP tools with PreToolUse. Use when asked to add Arcjet to a Claude Agent SDK or Claude Code agent, rate limit its tools, screen inbound messages, or block prompt injection / PII.
Integrate Arcjet security into a Vercel Eve agent using @arcjet/guard — add guard gates to tools and connections, screen inbound messages, and record agent lifecycle events correlated to the session. Use when asked to add Arcjet to an Eve agent, rate limit its tools, guard connection access, or screen inbound messages.
Integrate Arcjet security into a Genkit JS agent using @arcjet/guard — wrap ai.defineTool, put guardMiddleware on generate({ use }) for unwrapped / MCP / filesystem tools, and read a caller-owned id from generate({ context }). Use when asked to add Arcjet to genkit, rate limit its tools, screen inbound messages, or block prompt injection / PII.
Integrate Arcjet security into a LangChain JS createAgent using @arcjet/guard — wrap tool() / StructuredTool, put guardMiddleware on createAgent({ middleware }) for MCP / unwrapped tools, and read configurable.thread_id for correlation. Use when asked to add Arcjet to langchain createAgent, rate limit its tools, screen inbound messages, or block prompt injection / PII. This is LangChain JS, not the Python page.
Integrate Arcjet security into a Mastra agent using @arcjet/guard — wrap createTool execute, screen input/output with a Processor tripwire, and gate unwrapped MCP/workspace tools with hooks. Use when asked to add Arcjet to a Mastra agent, rate limit its tools, screen inbound messages, or block prompt injection / PII.
Integrate Arcjet security into an OpenAI Agents text Agent using @arcjet/guard — wrap tool({ execute }), screen inbound before run(), and read a caller-owned id from runContext.context. Use when asked to add Arcjet to @openai/agents, rate limit its tools, screen inbound messages, or block prompt injection / PII.
Integrate Arcjet security into a Strands Agents JS app using @arcjet/guard — wrap tool({ callback }), put guardHooks on Agent({ plugins }) for unwrapped / MCP / vended tools, and read a caller-owned id from invocationState. Use when asked to add Arcjet to strands-agents, rate limit its tools, screen inbound messages, or block prompt injection / PII.