arcjet/arcjet-js/integrate-arcjet-guard-claude-agent-sdk

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

Install

npx vskill@latest install arcjet/arcjet-js/integrate-arcjet-guard-claude-agent-sdk

SKILL.md

Integrate Arcjet Guard into a Claude Agent SDK agent

@arcjet/guard's Claude Agent SDK v0 namespace wraps the agent's existing Arcjet client. It never talks to the Arcjet API itself. Three surfaces, one decision rule:

  • An authored tool (tool() + createSdkMcpServer()) → guardTool().

DENY is a CallToolResult with isError: true. Do not throw.

  • Inbound text → guardHooks() UserPromptSubmit. DENY is

{ decision: "block" }. Timeout already fail-closes the prompt (Claude Code v2.1.208+).

  • Built-ins / unwrapped MCP → guardHooks() PreToolUse with

permissionDecision: "deny". Timeout already fail-closes (the tool does not run). PostToolUse captures the call.

  • What a tool returned → guardTool({ resultRules }) for an authored

tool, guardHooks({ toolResults }) for every tool. A DENY keeps the result from the model — see "Screen tool results".

  • Correlation → claudeAgentContext() reads session_id from hook

input or options.sessionId. Subagents have agent_id (metadata only). It never mints a new id. `options.sessionId` must be a UUID and can only be created once — see Step 5.

Screen inbound with UserPromptSubmit

This is the only place a turn can be declined before the model sees the prompt. There is no guardInbound. Put detectPromptInjection on guardHooks({ inbound }). A DENY returns { decision: "block", reason } and Claude Code erases the prompt.

canUseTool is not a policy gate

Claude's docs say canUseTool is skipped by allowedTools, allow rules, and bypassPermissions / acceptEdits. There is no guardCanUseTool. Do not put Arcjet policy on canUseTool. Same trap as Google ADK requireConfirmation and Cloudflare Think needsApproval: after a human yes, Guard still runs.

PreToolUse is the only deny for unwrapped tools

Built-ins (Bash, Write, …) and MCP tools you did not pass through guardTool are gated here. Annotations (readOnlyHint, …) and sandbox settings are not enforcement. PostToolUse cannot undo a tool that already ran; it can only keep the result from the model.

Screen tool results

Untrusted text mostly arrives in tool results: a fetched page, a file, an MCP response. Two options screen it before the model reads it:

  • guardTool(..., { resultRules }) — a second guard call over the

handler's result, labelled resultAction (default ${action}.result). resultRules receives { input, result, text }, where text is every text block in the result, never shortened. A DENY returns the same isError envelope in place of the result.

  • guardHooks({ toolResults }) — a PostToolUse screen over every tool.

toolResults.rules receives { toolName, input, response, text }. The label defaults to "tool.result".

ts
guardHooks(arcjet, {
sessionId,
toolResults: {
rules: ({ text }) => [detectPromptInjection()(text)],
},
});

On DENY, or an unevaluated policy under the default onGuardError: "deny":

ToolOn DENY
Any MCP tool (mcp__…)Result replaced with the denial
Bash, Read (REPLACEABLE_BUILTIN_TOOLS)Result replaced, in that tool's output shape
Any other toolThe run stops (continue: false)

Claude Code validates a built-in tool's replacement against an unpublished schema and sends the original result when it does not match, so only tools a test drives through the bundled CLI are replaced; every other tool stops the run. A stopped session still holds the original result: do not resume it. Its capture records claude.denial: "run_stopped". Where stopping is not acceptable, keep the tools canReplaceResult() rejects out of allowedTools. Subagent (Agent) output arrives later and is not screened. PostToolUse hooks run in parallel and the last rewrite wins, so another hook that rewrites results can replace the denial. A detector that cannot return a verdict in time on a long result leaves the policy unevaluated, which denies under the default onGuardError.

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. Built-ins and unwrapped MCP get guardHooks PreToolUse.

  1. What limits? (e.g. "10 lookups/min per order" → tokenBucket.)
  2. Who is the user for metadata — an opaque user/tenant ID (never PII)?

Default: none. Pass it via metadata on the policy. Session id is the correlation id, not the user.

  1. Is an Arcjet outage unacceptable? Every helper defaults to

onGuardError: "deny". Ask explicitly about inbound UserPromptSubmit: failing closed there means the agent stops answering for the duration of the outage, so "allow" is a routine and legitimate choice at that one call site.

The seven things readers get wrong

  1. There is no `guardInbound`. Screen prompt injection on

guardHooks({ inbound }) via UserPromptSubmit.

  1. `canUseTool` is not a policy gate. It is skipped by allowedTools,

allow rules, and bypassPermissions / acceptEdits. Use guardTool or PreToolUse.

  1. The import path is versioned and there is no alias.

@arcjet/guard/claude-agent-sdk/v0. @arcjet/guard/claude-agent-sdk does not resolve.

  1. `options.sessionId` is a UUID, and only once. A non-UUID exits with

"Invalid session ID"; reusing one exits with "already in use". Mint a UUID for the conversation and resume it on later turns.

  1. Correlation is read, never minted. Do not call createAgentContext

inside a Claude callback — that generates a second id and splits the Sequence. claudeAgentContext reads session_id / options.sessionId and omits correlationId when neither is a valid id. Subagent agent_id is metadata, not the correlation id.

  1. **Do not double-wrap with @arcjet/guard/vercel-ai/v7 or

@arcjet/guard/claude-managed-agents/v0.** There is no public @arcjet/guard/agents export. Claude tools are tool(), not AI SDK tool(), and this is not hosted Claude Managed Agents. guardTool throws if the tool already carries the Arcjet protection brand. Applying guardTool and guardHooks PreToolUse to the same authored tool double-calls the guard.

  1. A denial from `guardTool` is a `CallToolResult` with `isError: true`,

not a throw. If onDeny throws, the handler still does not run and the model still receives the default denial result.

Step 1: Install and find the guard client

Install @arcjet/guard (required), plus @anthropic-ai/claude-agent-sdk (optional peer, needed for @arcjet/guard/claude-agent-sdk/v0). Always use the versioned path: @arcjet/guard/claude-agent-sdk/v0 resolves; @arcjet/guard/claude-agent-sdk throws ERR_PACKAGE_PATH_NOT_EXPORTED.

sh
npm install @arcjet/guard @arcjet/sensitive-info-rampart @anthropic-ai/claude-agent-sdk

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 "@anthropic-ai/claude-agent-sdk";
import { tokenBucket, localDetectSensitiveInfo, policyInput } from "@arcjet/guard";
import { guardTool } from "@arcjet/guard/claude-agent-sdk/v0";
import { rampart } from "@arcjet/sensitive-info-rampart";
import { z } from "zod";
import { arcjet } from "./arcjet.js";
const lookupLimit = tokenBucket({
bucket: "lookups",
refillRate: 10,
intervalSeconds: 60,
maxTokens: 10,
});
const detectPii = localDetectSensitiveInfo({ backend: rampart() });
export const lookupOrder = guardTool(
arcjet,
tool(
"lookup_order",
"Look up an order by ID",
{
orderId: z.string(),
note: z.string(),
},
async ({ orderId, note }) => ({
content: [{ type: "text", text: `${orderId}: shipped (${note})` }],
}),
),
{
action: "order.looked-up",
actor: userId,
inputs: (input) => ({
orderId: policyInput.server.string(input.orderId),
}),
rules: (input) => [lookupLimit({ key: input.orderId, requested: 1 }), detectPii(input.note)],
},
);
  • Omit rules to submit none. The guard call still happens.
  • 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.

  • On DENY the tool's handler never runs. The model receives

{ content, structuredContent: { arcjetDenied, reason, message, retryable }, isError: true }.

  • Default onGuardError: "deny" blocks the tool if Arcjet is unreachable,

or if a rules, metadata or sessionId callback throws or returns a value Guard cannot use. A failed callback never skips the guard call: Guard is called without its value, so remote policy still evaluates the call and a DENY stands.

  • Pass the same sessionId you give query({ options.sessionId }) on the

policy when the handler extra does not carry session_id.

Step 3: Screen inbound with UserPromptSubmit

ts
import { query } from "@anthropic-ai/claude-agent-sdk";
import { detectPromptInjection } from "@arcjet/guard";
import { guardHooks } from "@arcjet/guard/claude-agent-sdk/v0";
import { arcjet } from "./arcjet.js";
const sessionId = conversationId;
for await (const message of query({
prompt: userText,
options: {
sessionId,
hooks: guardHooks(arcjet, {
sessionId,
inbound: {
action: "message.received",
rules: ({ prompt }) => [detectPromptInjection()(prompt)],
},
}),
},
})) {
void message;
}
  • On DENY, UserPromptSubmit returns { decision: "block", reason } and

the prompt is erased. The model never sees it.

  • Default onGuardError: "deny" — if the guard cannot be evaluated, or an

inbound action, rules or metadata callback throws or returns a value Guard cannot use, the prompt is blocked. Guard is still called without the failed value (a failed action falls back to "message.received"), so a DENY stands. Use "allow" on inbound when the human cost of rejecting a legitimate message exceeds the security cost of an outage.

Step 4: Gate tools you did not wrap

ts
import { detectPromptInjection, tokenBucket } from "@arcjet/guard";
import { guardHooks } from "@arcjet/guard/claude-agent-sdk/v0";
import { arcjet } from "./arcjet.js";
const mcpLimit = tokenBucket({
bucket: "mcp-access",
refillRate: 20,
intervalSeconds: 60,
maxTokens: 20,
});
export const hooks = guardHooks(arcjet, {
sessionId: conversationId,
action: ({ toolName }) => `${toolName}.invoked`,
rules: ({ toolName }) => [mcpLimit({ key: toolName, requested: 1 })],
// Tools that already guard themselves via `guardTool`. Without this they
// are guarded twice per invocation.
exclude: [{ server: "support", name: "lookup_order" }],
inbound: {
action: "message.received",
rules: ({ prompt }) => [detectPromptInjection()(prompt)],
},
});

Pass hooks to query({ options.hooks }). PreToolUse returns permissionDecision: "deny" so Bash / Write / unwrapped MCP never execute. PostToolUse records the call even when an action or metadata callback fails, under the default label and its own metadata, and blocks nothing unless toolResults is set.

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

7-day trend score
5

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-claude-agent-sdk/badge)](https://verified-skill.com/skills/arcjet/arcjet-js/integrate-arcjet-guard-claude-agent-sdk)

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