integrate-arcjet-guard-agents
Integrate 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.
git clone --depth 1 https://github.com/arcjet/arcjet-js /tmp/integrate-arcjet-guard-agents && cp -r /tmp/integrate-arcjet-guard-agents/arcjet-guard/skills/integrate-arcjet-guard-agents ~/.claude/skills/integrate-arcjet-guard-agentsSKILL.md
# Integrate Arcjet Guard into a Vercel AI SDK app
`@arcjet/guard`'s Vercel AI v7 namespace wraps the app's existing Arcjet
client. It never talks to the Arcjet API itself. Three surfaces, one decision
rule:
- **Model-invoked** (the LLM decides to call a tool) → `guardTool()`
- **App-invoked** (your code performs a risky action) → `guardAction()`
- **Observe-only** (record that something happened) → `captureAction()`
All three attach the same correlation ID so the Arcjet Console reconstructs
the whole run as one Sequence.
## Questions to ask the human first
Ask only what you cannot infer from the code; suggest defaults.
1. Which tool calls / actions are **risky** (external side effects,
irreversible, spends money, sends messages)? Those get rules. Purely
informational ones can be wrapped with no `rules` (recorded, nothing
enforced locally) or left to `captureAction()`.
2. What **limits**? (e.g. "10 lookups/min per user" → `tokenBucket`;
"5 posts/min" → `slidingWindow`.)
3. Who is the **user** for metadata — an opaque user/tenant/installation ID
(never PII)?
4. Is there an existing **run identifier** (request ID, job ID, review ID)
to use as the correlation ID? Default: auto-generated ULID.
## Step 1: Install and find the guard client
Install `@arcjet/guard` (required), plus `ai` and `@ai-sdk/provider-utils`
(optional peers, needed only for `@arcjet/guard/vercel-ai/v7`). Every agent
helper lives on that one path. Always use explicit versions:
`@arcjet/guard/vercel-ai/v7` resolves, but `@arcjet/guard/vercel-ai` does not —
omitting the version is deliberate (it prevents silent API breaking changes
when a new major version is supported). Attempting to import from an
unversioned path throws `ERR_PACKAGE_PATH_NOT_EXPORTED`.
```sh
npm install @arcjet/guard ai @ai-sdk/provider-utils
```
If the app 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: Create the context at the run's entry point
In the HTTP route / job handler / webhook that starts the run:
```ts
import { createAgentContext, securityMetadata } from "@arcjet/guard/vercel-ai/v7";
const ctx = createAgentContext({
correlationId: existingRunId, // omit to auto-generate a ULID
metadata: securityMetadata({ agent: "support-agent", workflow: "support-request", user: userId }),
});
```
Constraints: correlation IDs are 1–256 characters of printable ASCII;
invalid values throw at creation.
The `correlationId` joins every guard decision and capture event from one
logical run **or session** into a single sequence in the Arcjet console, so
the best value is an ID the app already has and can search by (request ID,
job ID, ticket ID, review ID). Omit it and a ULID is generated.
## Step 3: Thread the context explicitly
The context is a plain JSON-serializable object. Pass it hand to hand — as a
field on queue payloads and workflow inputs (it survives serialization).
Never stash it in module state or AsyncLocalStorage.
## Step 4: Wrap model-invoked tools
```ts
import { guardTool, securityMetadata } from "@arcjet/guard/vercel-ai/v7";
import { tokenBucket } from "@arcjet/guard";
const lookupLimit = tokenBucket({ bucket: "lookups", refillRate: 5, intervalSeconds: 60, maxTokens: 10 });
const tools = {
lookupOrder: guardTool(arcjet, lookupOrderTool, {
action: "order.looked-up", // "resource.verb", past tense
rules: ({ orderNumber }) => [lookupLimit({ key: `order:${orderNumber}`, requested: 1 })],
// securityMetadata() maps the flat vocabulary to wire keys, so its fields
// are strings. Nested values go alongside it in the raw metadata object.
metadata: (input) => ({
...securityMetadata({ resource: `order:${input.orderNumber}`, user: userId }),
caller: { id: userId, role: "customer" },
}),
}),
};
```
- Omit `rules` to submit none. The guard call still happens, so the decision is
correlatable and the call site stays reachable by policy configured outside
the code — but it costs a round trip. Use `captureAction()` instead when you
want a record and no decision.
- `rules` may be a callback over the tool's parsed input, computed from the
data being acted on — here, keying the rate limit on the specific order
being looked up.
- On DENY the tool's `execute` never runs; the model receives a structured
denial result carrying the deciding rule's own `reason` — for the
`tokenBucket` above that is `reason: "RATE_LIMIT"`, `retryable: true`, and a
computed `retryAfterSeconds`. Only rate-limit denials are retryable; every
other reason reports `retryable: false` and no backoff hint. Reshape it with
`onDeny`.
- Guard policy unavailability: if the guard cannot be evaluated (e.g. Arcjet
API unreachable), the default is `onGuardError: "deny"` — the tool is blocked
and the model receives `reason: "ERROR"` with `retryable: true` and a fixed
`retryAfterSeconds: 5` backoff hint. For read-only operations like lookups,
set `onGuardError: "allow"` if availability matters more than enforcement: the
tool executes normally and the model receives its ordinary output.
- Pilot limitation: `guardTool` throws if the tool already declares its
own `contextSchema`.
- **Alternative form:** calling `guardAction` directly inside the tool's
`execute` block is also supported and keeps control flow visible, but
requires threading the context in by hand. `guardTool` wrapping extracts it
automatically via the injected `contextSchema`.
## Step 5: Deliver the context to the tools
```ts
import { aiToolsContext } from "@arcjet/guard/vercel-ai/v7";
const result = await generateText({
model,
instructions:
systemPrompt +
" If a tool call is denied by security policy, do not retry it; explain the denial to the user or try a different approach.",
prompt,
tools,
toolsContext: aiToolsContext(ctx, tools),
stopWhen: stepCountIntegrate 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 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.
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.