@arcjet/skills
guard
Add Arcjet Guard to non-HTTP JavaScript: agent tool calls, MCP handlers, queue workers, and background jobs. Use when there is no HTTP request object, or when the user asks to guard tools, rate-limit agent actions, or block prompt injection on tool arguments.
Guard non-HTTP JavaScript
Use @arcjet/guard when there is no HTTP request. MCP tools, queue workers, and agent tool calls are Guard. HTTP routes are @arcjet/skills#protect.
For a specific vendor SDK, load the matching skill from @arcjet/guard (@arcjet/guard#integrate-arcjet-guard-agents, -eve, -mastra, -langgraph, -langchain, -openai-agents, -genkit, -google-adk, -strands-agents, -tanstack-ai, -cloudflare-think, -claude-agent-sdk, -claude-managed-agents).
Claude Code / Copilot HTTP hooks are not this package. Publish a coding-agent policy via @arcjet/skills#mcp (Execute on: Tool call, Prompt, or Model switch) and install hooks from https://docs.arcjet.com/coding-agents (omit ?surface=).
Client
import { launchArcjet } from "@arcjet/guard";
export const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });One client at module scope. Get the key with @arcjet/skills#cli first. Declare rules at module scope so .deniedResult(decision) works.
One guard() per operation
Hardcode the label. Do not interpolate in a generic dispatcher.
Hardcode labels as slugs: ASCII letters of either case, digits, -, ., and _ (tools.get-weather). Start and end with a letter or digit; max 256 bytes. Labels match case-sensitively. Prefer dash/dot in new labels. Check a label you build yourself with validateGuardLabel.
async function getWeather(userId: string) {
const decision = await arcjet.guard({
label: "tools.get-weather",
rules: [/* ... */],
metadata: { user: { id: userId } },
});
if (decision.conclusion === "DENY") {
const rateLimited = toolCallLimit.deniedResult(decision);
if (rateLimited) {
return { error: `rate limited, retry after ${rateLimited.resetAtUnixSeconds}` };
}
return { error: decision.reason };
}
if (decision.hasFailedOpen()) {
// ALLOW only because a rule could not run — deny here if the site is sensitive
}
// ... run the tool
}Core guard() fails open — check hasFailedOpen(). warnings never change the conclusion. Rate-limit rules need key and bucket. Nested metadata is Console-only; no secrets or PII. decision.reason is a flat string on DENY and undefined on ALLOW.
Use localDetectSensitiveInfo({ backend: rampart() }) from @arcjet/sensitive-info-rampart for PII (names, addresses, government IDs, card, email, phone, IP). Always pass the backend on the rule – it doesn't inherit the client's sensitiveInfoBackend, so share one instance. On Cloudflare Workers or edge runtimes, pass rampart() a classify function. moderateContent() is Guard-only.
capture() is visibility, never a deny. flush() on shutdown.
Versioned wrappers
Import @arcjet/guard/<vendor>/v<major> — unversioned paths do not resolve. Wrappers fail closed by default. HITL (needsApproval, interrupt(), requireConfirmation, canUseTool) is not a policy gate; Guard still runs after a human yes.
Every wrapper policy accepts optional actor and inputs (static or a resolver over that adapter's native call — parsed input plus trusted runtime/context) so a remote policy that declares those names can evaluate. Build each input with policyInput. Omit them and the remote policy has nothing to read — its rules do not fire.
Google ADK is guardPlugin (no guardTool). TanStack AI is guardMiddleware (do not wrap execute). LangChain createAgent is /langchain/v1, not LangGraph.
Load @arcjet/skills#choose-protections to pick rules. Exact signatures live in the installed @arcjet/guard types.