arcjet/arcjet-js/integrate-arcjet-guard-agents
Claude Code and agent skill by arcjet/arcjet-js
- community
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-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. 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.
- 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().
- What limits? (e.g. "10 lookups/min per user" →
tokenBucket;
"5 posts/min" → slidingWindow.)
- Who is the user for metadata — an opaque user/tenant/installation ID
(never PII)?
- 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.
If the app has no guard client yet, launch one once at module scope:
Step 2: Create the context at the run's entry point
In the HTTP route / job handler / webhook that starts the run:
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
- Optional
actorandinputs(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
rulesto 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.
rulesmay 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
executenever 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
rulesormetadatacallback 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:
guardToolthrows if the tool already declares its
own contextSchema.
- Alternative form: calling
guardActiondirectly 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
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
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:
| Limit | Value | Over the limit | |
|---|---|---|---|
| Top-level keys | 128 | extra keys dropped | |
| Serialized bytes per value | 4 KiB | that key dropped | |
| Nesting depth per value | 10 | that key dropped | |
| Key names | letters, 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
- SKILL.md in the repositoryCheckingarcjet-guard/skills/integrate-arcjet-guard-agents/SKILL.md
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
[](https://verified-skill.com/skills/arcjet/arcjet-js/integrate-arcjet-guard-agents)