Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ jobs:
node --test 'arcjet-guard/src/vercel-eve/**/*.test.ts'
working-directory: ${{ github.workspace }}

# The claude-agent-sdk namespace imports the SDK for types only. A static
# scan proves the imports are written `import type`; this proves the
# consequence, catching a build step that re-emits one as a value import.
#
# Runs before the eve-absent step because that one deletes a different
# optional peer. Typecheck legitimately fails without the SDK.
- name: Unit tests with claude-agent-sdk absent
run: |
rm -rf node_modules/@anthropic-ai/claude-agent-sdk arcjet-guard/node_modules/@anthropic-ai/claude-agent-sdk
node -e "try { require.resolve('@anthropic-ai/claude-agent-sdk', { paths: ['arcjet-guard/src/claude-agent-sdk/v0'] }); console.error('claude-agent-sdk still resolves'); process.exit(1) } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error }"
count=$(find arcjet-guard/src/claude-agent-sdk -name '*.test.ts' | wc -l)
test "$count" -ge 5 || { echo "only $count claude-agent-sdk test files matched; the glob has gone stale" >&2; exit 1; }
node --test 'arcjet-guard/src/claude-agent-sdk/**/*.test.ts'
working-directory: ${{ github.workspace }}

# The mastra namespace imports @mastra/core for types only. A static scan
# proves the imports are written `import type`; this proves the
# consequence, catching a build step that re-emits one as a value import.
Expand Down
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ change them:
`mastra/v1` is the same idea on a different SDK: Mastra already runs
channels through `processInput` and treats `requireApproval` as human HITL,
so its helpers are `guardTool`, `guardProcessor`, and `guardHooks`.
`claude-agent-sdk/v0` is the same idea again: authored tools are `tool()`
handlers, inbound is `UserPromptSubmit`, and unwrapped built-ins are
`PreToolUse` — `canUseTool` is not a policy gate.
- **Flat** — a single level under `@arcjet/guard`, no further nesting.
- **Explicitly versioned, with no unversioned alias.** `@arcjet/guard/vercel-ai`
does not resolve, and neither does a wildcard `./vercel-ai/*`. An alias would
Expand Down
84 changes: 81 additions & 3 deletions arcjet-guard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,64 @@ helper. Currently available:
export default defineHook(arcjetHooks(arcjet));
```

- **`@arcjet/guard/claude-agent-sdk/v0`** — Claude Agent SDK v0 integration.
Exports `guardTool`, `guardHooks`, and `claudeAgentContext`. There is no
`guardInbound` (inbound is `UserPromptSubmit` on `guardHooks`) and no
`canUseTool` helper (`canUseTool` is skipped by `allowedTools`, allow
rules, and `bypassPermissions` / `acceptEdits`):

```ts
import { launchArcjet, detectPromptInjection, tokenBucket } from "@arcjet/guard";
import { guardTool, guardHooks } from "@arcjet/guard/claude-agent-sdk/v0";
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";

const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
const limit = tokenBucket({
refillRate: 10,
intervalSeconds: 60,
maxTokens: 10,
});

const lookupOrder = guardTool(
arcjet,
tool(
"lookup_order",
"Look up an order",
{ orderNumber: z.string() },
async ({ orderNumber }) => ({
content: [{ type: "text", text: `${orderNumber}: shipped` }],
}),
),
{
action: "order.looked-up",
onGuardError: "deny",
rules: (input) => [limit({ key: input.orderNumber, requested: 1 })],
},
);

const sessionId = conversationId;

for await (const message of query({
prompt: userText,
options: {
sessionId,
mcpServers: {
app: createSdkMcpServer({ name: "app", tools: [lookupOrder] }),
},
hooks: guardHooks(arcjet, {
sessionId,
inbound: {
action: "message.received",
rules: ({ prompt }) => [detectPromptInjection()(prompt)],
},
}),
},
})) {
void message;
}
```

- **`@arcjet/guard/mastra/v1`** — Mastra v1 integration. Exports `guardTool`,
`guardProcessor`, `guardHooks`, and `mastraAgentContext`. There is no
`guardInbound` (channels already hit `processInput`) and no `guardApproval`
Expand Down Expand Up @@ -1016,6 +1074,9 @@ importing only core guards are not forced to install unneeded packages:
Eve, ensure your deployment environment and CI both run Node 24 or later.
- **`@arcjet/guard/mastra/v1`** requires `@mastra/core` (optional peer,
installed only to use `@arcjet/guard/mastra/v1`). The peer range is `>=1 <2`.
- **`@arcjet/guard/claude-agent-sdk/v0`** requires
`@anthropic-ai/claude-agent-sdk` (optional peer, installed only to use
`@arcjet/guard/claude-agent-sdk/v0`). The peer range is `>=0.1.0 <1`.

**pnpm caveat**: pnpm does not reliably honour
`peerDependenciesMeta.*.optional` (pnpm#5152, #8142), especially with
Expand All @@ -1040,6 +1101,11 @@ pnpm install eve
pnpm install @mastra/core
```

```sh
# @arcjet/guard/claude-agent-sdk/v0
pnpm install @anthropic-ai/claude-agent-sdk
```

```sh
# or skip the peer install and relax the check:
pnpm install --no-strict-peer-dependencies
Expand All @@ -1052,8 +1118,9 @@ are not tied to any AI SDK, and internally they are kept that way — nothing
they import reaches `ai`. They are published on each vendor namespace, so there
is one path to learn and no layering to reason about.

`@arcjet/guard/vercel-ai/v7`, `@arcjet/guard/vercel-eve/v0`, and
`@arcjet/guard/mastra/v1` now export these helpers. The open next step is
`@arcjet/guard/vercel-ai/v7`, `@arcjet/guard/vercel-eve/v0`,
`@arcjet/guard/mastra/v1`, and `@arcjet/guard/claude-agent-sdk/v0` now export
these helpers. The open next step is
promoting them to the root `@arcjet/guard` export so a caller can get the
agnostic layer without installing a vendor peer. That change is a follow-up
with its own ADR; there is still no public `@arcjet/guard/agents`.
Expand All @@ -1073,6 +1140,7 @@ with its own ADR; there is still no public `@arcjet/guard/agents`.
| `guardTool` / `guardAction` | Deny (fail closed) | `onGuardError: "allow"` |
| Eve `guardInbound` / `guardApproval` | Deny (fail closed) | `onGuardError: "allow"` |
| Mastra `guardProcessor` / `guardHooks` | Deny (fail closed) | `onGuardError: "allow"` |
| Claude `guardTool` / `guardHooks` | Deny (fail closed) | `onGuardError: "allow"` |

`onGuardError` is broader than Arcjet Cloud availability. It governs both an
unexpected throw from `guard()` and an ALLOW decision whose `hasFailedOpen()`
Expand Down Expand Up @@ -1425,7 +1493,7 @@ For an example with Mastra, see [`mastra-agent`](https://github.com/arcjet/examp

## Agent skill

For integration help in Claude Code or other AI coding agents, three skill files are packaged with `@arcjet/guard`:
For integration help in Claude Code or other AI coding agents, four skill files are packaged with `@arcjet/guard`:

**For Vercel AI SDK:**

Expand Down Expand Up @@ -1459,6 +1527,16 @@ ln -s /path/to/node_modules/@arcjet/guard/skills/integrate-arcjet-guard-mastra ~

In Claude Code, use `/integrate-arcjet-guard-mastra` to start an integration session.

**For the Claude Agent SDK:**

```bash
cp -r node_modules/@arcjet/guard/skills/integrate-arcjet-guard-claude-agent-sdk ~/.claude/skills/
# or
ln -s /path/to/node_modules/@arcjet/guard/skills/integrate-arcjet-guard-claude-agent-sdk ~/.claude/skills/
```

In Claude Code, use `/integrate-arcjet-guard-claude-agent-sdk` to start an integration session.

Each skill guides you through wrapping tools, screening inbound messages, and recording lifecycle events joined by correlation ID.

Note: `npx skills add arcjet/skills` refers to the separate Anthropic skills marketplace, not the packaged file.
Expand Down
9 changes: 9 additions & 0 deletions arcjet-guard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@
"./mastra/v1": {
"types": "./dist/mastra/v1/index.d.ts",
"import": "./dist/mastra/v1/index.js"
},
"./claude-agent-sdk/v0": {
"types": "./dist/claude-agent-sdk/v0/index.d.ts",
"import": "./dist/claude-agent-sdk/v0/index.js"
}
},
"publishConfig": {
Expand Down Expand Up @@ -108,6 +112,7 @@
},
"devDependencies": {
"@ai-sdk/provider-utils": "5.0.13",
"@anthropic-ai/claude-agent-sdk": "0.3.221",
"@mastra/core": "1.58.0",
"@types/node": "22.20.1",
"ai": "7.0.38",
Expand All @@ -119,6 +124,7 @@
},
"peerDependencies": {
"@ai-sdk/provider-utils": ">=5 <6",
"@anthropic-ai/claude-agent-sdk": ">=0.1.0 <1",
"@mastra/core": ">=1 <2",
"ai": ">=7 <8",
"eve": ">=0.25.1 <1"
Expand All @@ -135,6 +141,9 @@
},
"@mastra/core": {
"optional": true
},
"@anthropic-ai/claude-agent-sdk": {
"optional": true
}
},
"engines": {
Expand Down
Loading
Loading