arcjet/arcjet-js/integrate-arcjet-guard-agents

Claude Code and agent skill by arcjet/arcjet-js

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

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.

Install

npx vskill@latest install arcjet/arcjet-js/integrate-arcjet-guard-agents

SKILL.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. This is not hosted Claude Managed Agents (@arcjet/guard/claude-managed-agents/v0).

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

  1. What limits? (e.g. "10 lookups/min per user" → tokenBucket;

"5 posts/min" → slidingWindow.)

  1. Who is the user for metadata — an opaque user/tenant/installation ID

(never PII)?

  1. 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 { tokenBucket, policyInput } from "@arcjet/guard";
import { guardTool, securityMetadata } from "@arcjet/guard/vercel-ai/v7";
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
actor: userId,
inputs: ({ orderNumber }) => ({
orderNumber: policyInput.server.string(orderNumber),
}),
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" },
}),
}),
};
  • Optional actor and inputs (static, or a resolver over this adapter's native call — parsed input plus trusted runtime/context) are forwarded on the

guard call so a remote policy that declares those names can evaluate. Build each input with policyInput. A resolver that throws or returns a value Guard cannot use is left out of the guard call, and onGuardError decides.

  • 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 no helper produced; pass decision.id as its decisionId to join that record to a guard() call the application made itself.

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

  • To screen PII in a free-text argument, add

localDetectSensitiveInfo({ backend: rampart() })(input.note) to rules. rampart comes from @arcjet/sensitive-info-rampart, which detects PII on-device and needs a server runtime (Node.js, Bun, or Deno).

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

  • A rules or metadata callback that throws, or returns a value Guard

cannot use, never skips the guard call: Guard is called without that value (no local rules, or no policy metadata), so remote policy still evaluates the call and a DENY stands. Otherwise onGuardError decides as above, and the capture records outcome: "unavailable" under "deny" or outcome: "degraded" under "allow". rules must return an array of rules bound to their input.

  • 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: stepCountIs(5),
});

Works identically with streamText and ToolLoopAgent, and inside a Vercel Workflow ("use workflow") as the example shows — the wrapper only changes the tool's own behavior. Always add the denial line to the system prompt (shown above).

The compiler will NOT catch a missing `toolsContext`. The injected context type includes undefined (so uncorrelated calls still run, fail-open), which makes the toolsContext option optional at the type level. Forget it and guard checks run uncorrelated: the first uncorrelated call always warns, but further ones are silent unless ARCJET_LOG_LEVEL is set — so run once with ARCJET_LOG_LEVEL=warn and confirm correlation before shipping.

Step 6: Wrap app-invoked actions; capture side effects

ts
import {
ArcjetDeniedError,
ArcjetGuardUnavailableError,
captureAction,
guardAction,
} from "@arcjet/guard/vercel-ai/v7";
try {
await guardAction(
arcjet,
ctx,
{
action: "review.submitted",
rules: [submitLimit({ key: repoId })],
metadata: securityMetadata({ destination: "github", reversibility: "compensable" }),
},
async () => {
// Example: calling an external service (e.g. GitHub API client)
return await externalServiceClient.pulls.createReview({
owner: repoOwner,
repo: repoName,
pull_number: prNumber,
body: reviewText,
});
},
);
} catch (error) {
if (error instanceof ArcjetDeniedError) {
// A rule denied the call. Tell the user why; do not retry.
console.warn("denied:", error.decision.reason);
} else if (error instanceof ArcjetGuardUnavailableError) {
// The policy could not be evaluated. Distinct from a denial, and usually
// the one worth alerting on.
console.warn("policy unavailable for:", error.action);
} else {
throw error;
}
}
captureAction(arcjet, ctx, {
action: "notification.sent",
metadata: securityMetadata({ destination: "slack" }),
});

guardAction throws ArcjetDeniedError (carrying the decision) on DENY and ArcjetGuardUnavailableError (carrying the decision or cause) when the guard policy could not be evaluated — decide with the human whether to catch-and-skip or let it abort. The two error types are distinct so an unavailable guard can be alerted on separately from a DENY decision. ArcjetGuardUnavailableError carries cause (the guard call threw) or decision (a decision failed open), making the two distinguishable in a handler. The fail-closed tool result carries a fixed retryAfterSeconds: 5 backoff hint. The capture outcome on that path is "unavailable", not "denied", so an operator can query the two separately. The layering resolves a potential confusion: the core @arcjet/guard client still fails open by construction and _reports_ it via hasFailedOpen(); these helpers _decide_ to block on it.

Metadata vocabulary

Use securityMetadata() keys consistently: user (whose authority — opaque ID), agent (which automated actor), workflow (logical workflow name), dataClass (public/internal/confidential/regulated), destination (where effects go: github, slack, internal), reversibility (reversible/compensable/irreversible), resource (what's acted on, e.g. repo:owner/name#123). The action is not metadata — it is the guard label / capture action: resource.verb past tense (order.looked-up), validated server-side: ASCII letters of either case, digits, dash, dot, and underscore, starting and ending with a letter or digit, at most 256 bytes. A label is matched case-sensitively, so Order.looked-up and order.looked-up are different labels.

Metadata accepts any JSON-serializable value — nested objects and arrays included. The server enforces the following limits, dropping keys that exceed them and reporting each drop on decision.warnings:

LimitValueOver the limit
Top-level keys128extra keys dropped
Serialized bytes per value4 KiBthat key dropped
Nesting depth per value10that key dropped
Key namesletters, digits, -, ., _that key dropped

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
681
Repository forks
31
7-day trend score
33

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/arcjet/arcjet-js/integrate-arcjet-guard-agents/badge)](https://verified-skill.com/skills/arcjet/arcjet-js/integrate-arcjet-guard-agents)

Something wrong with this skill or its page? Tell us.