# Arcjet
> Arcjet is the runtime security platform that ships in your AI code. Detect prompt injection, authorize agent tool calls, redact sensitive data, and block bots and abuse. Real-time security building blocks you call inside your app, before an action happens.
Arcjet is a lightweight SDK that enforces controls inline, with real identity and session context – configured by your agent with the CLI or MCP server.
Arcjet's primary use case is securing the actions AI agents take in production. Agents have gone from answering questions to moving money, changing records, and shipping code, and the security or engineering leader now owns that risk. Identity and RBAC authenticate the agent but don't govern the action it's about to take, and network proxies can't see inside the workflow. Arcjet gives security and engineering teams visibility into what each agent is doing, real-time enforcement before a consequential action (prompt injection, PII, tool authorization), and an audit trail.
Because it runs inside the same application code, Arcjet protects traditional entry points the same way – enforce budgets, detect bots, validate email, rate limit, and block common attacks across HTTP routes and APIs.
Arcjet protects two types of entry points:
- **Request-based** – HTTP route handlers, API endpoints, middleware. Use `protect()` with any supported framework.
- **Guards** – tool calls, queue consumers, agentic pipelines, and anywhere else you process untrusted input without an HTTP request. Use `guard()` to pass inputs directly and get a decision back. Use `capture()` to record that an allowed action happened (visibility only; never changes a decision).
Arcjet runs server-side. Bot protection advanced client signals are an optional
extra layer of defense. Pricing is based on usage, see https://arcjet.com/pricing
## Get started
Set up Arcjet in two steps: (1) install a skill that gives your agent the
documentation to integrate the Arcjet SDK, and (2) connect to Arcjet with the
CLI to create sites, retrieve credentials, and verify decisions.
Full guide: https://docs.arcjet.com/agent-get-started
### Step 1: Install the skill
The Arcjet skill gives your agent the documentation to detect your framework,
install the SDK, and wire up protection rules – for HTTP routes, tool calls,
MCP servers, queues, and more:
```bash
npx skills add arcjet/skills
```
Then describe what you want to protect. The skill handles the rest.
Source: https://github.com/arcjet/skills
You can also use the Arcjet plugin for Claude Code and Cursor, which bundles
skills, MCP, and coding rules: https://docs.arcjet.com/arcjet-plugin
### Step 2: Connect with the CLI
```bash
npm i -g @arcjet/cli
arcjet auth login
arcjet teams list
arcjet sites list --team-id team_01abc123
arcjet sites get-key --site-id site_01abc123
```
Full documentation: https://docs.arcjet.com/cli
You can also use the MCP server to manage sites and keys. See the MCP section
below for setup instructions.
## MCP Server
Endpoint: `https://api.arcjet.com/mcp`
Auth: OAuth (browser-based, automatic on first connection)
Claude Code:
```bash
claude mcp add arcjet --transport http https://api.arcjet.com/mcp
```
VS Code (Copilot) `.vscode/mcp.json`:
```json
{ "servers": { "arcjet": { "type": "http", "url": "https://api.arcjet.com/mcp" } } }
```
Cursor `.cursor/mcp.json`:
```json
{ "mcpServers": { "arcjet": { "type": "streamable-http", "url": "https://api.arcjet.com/mcp" } } }
```
Windsurf `mcp_config.json`:
```json
{ "mcpServers": { "arcjet": { "serverUrl": "https://api.arcjet.com/mcp" } } }
```
Full documentation: https://docs.arcjet.com/mcp-server
### Tools
- **List teams** you belong to.
- **List sites** within a team.
- **Create new sites** within a team.
- **Get site keys** (`ARCJET_KEY`) for use in your projects.
- **List requests** received by a site with optional filtering.
- **Get request details** including headers, rules executed, and decision info.
- **Explain decisions** to understand why requests were allowed or denied.
- **Get site quota** usage and limits for the current billing window.
- **Analyze traffic** patterns, denial rates, top paths, top IPs, and trend vs previous period.
- **Detect anomalies** by comparing current traffic to the previous period – traffic spikes, geographic shifts, new threats, suspicious IPs.
- **Investigate IPs** with geo location, ASN, threat intelligence, and per-site request activity.
- **Get dry-run impact** – see what would happen if dry-run rules were promoted to live (blocked requests, affected IPs, false-positive estimate).
- **Get a security briefing** – comprehensive daily overview combining traffic, threats, anomalies, dry-run readiness, quota, and recommendations.
- **List remote rules** configured for a site.
- **Create remote rules** with DRY_RUN or LIVE mode – no code changes needed.
- **Update remote rules** by replacing the full rule configuration.
- **Delete remote rules** to immediately stop evaluation.
- **Promote remote rules** from DRY_RUN to LIVE after verification.
### Typical workflow
**Setup:** list-teams → list-sites (or create-site) → get-site-key → set `ARCJET_KEY` in your environment.
**Investigate:** list-requests → get-request-details or explain-decision for a specific request.
**Analyze and monitor:** analyze-traffic for a Console-level overview → get-anomalies to detect unusual patterns → investigate-ip for deep-dive on suspicious IPs.
**Daily security briefing:** get-security-briefing for a comprehensive overview (traffic, threats, anomalies, dry-run readiness, quota, and recommendations) in a single call.
**Manage remote rules:** list-rules → create-rule (DRY_RUN) → get-dry-run-impact to check impact → promote-rule to LIVE.
**Update/delete rules:** list-rules → update-rule (full replacement) or delete-rule.
### Remote rules
Remote rules are managed via the MCP server or Console – no code changes or redeployment needed. They apply globally to all requests for a site. Supported types: rate_limit, bot, shield, filter. Rules needing request body content (email, sensitive_info, prompt_injection) require the SDK.
**Responding to an active attack:** The most common use case is blocking suspicious traffic immediately. For example, to block a specific country, VPN, or IP range during an attack:
1. `list-requests` – investigate traffic and identify patterns.
2. `create-rule` – add a filter rule in DRY_RUN. Examples: `ip.src.country == "XX"` (ISO 3166-1 alpha-2 code, such as `US`, `CN`, or `RU`), `ip.src.vpn`, `ip.src in { 1.2.3.0/24 }`.
3. `list-requests` – confirm the rule matches attack traffic, not legitimate users.
4. `promote-rule` – switch to LIVE to start blocking.
5. `delete-rule` – remove the block once the attack subsides.
## Use cases
### Request-based (HTTP route handlers)
- Protecting AI endpoints from cost abuse? → tokenBucket + detectBot (AI Endpoint Abuse Protection)
- Preventing data leaks from AI features? → sensitiveInfo (AI Data Loss Prevention)
- Blocking prompt injection? → detectPromptInjection
- Responding to an active attack? → remote rules via MCP or CLI
### Guards (tool calls, agentic pipelines, queues)
- Securing MCP server tool handlers? → guard() with rate limiting + prompt injection detection
- Rate limiting per-user tool calls? → guard() with tokenBucket
- Scanning tool inputs/outputs for PII? → guard() with `localDetectSensitiveInfo()`
(JS) or `LocalDetectSensitiveInfo()` (Python). The HTTP `sensitiveInfo` /
`detect_sensitive_info` rule is not exported by the Guard SDK. Pass `allow`
or `deny`, and a `backend` for any entity type beyond the default four.
- Detecting prompt injection in agent tool results? → guard() with detectPromptInjection
- Recording that an allowed action happened? → capture() / Capture (batched, best-effort)
- Moderating untrusted text at a tool boundary? → moderateContent() (JS), ModerateContent() (Python), or GuardModerateContent (Go).
- Using an agent framework that owns the tool loop? → the framework adapter, not a raw guard() call. See "Agent framework adapters".
Add guard protection with the skill:
```bash
npx skills add arcjet/skills
```
Source: https://github.com/arcjet/skills
JS/TS SDK: https://github.com/arcjet/arcjet-js
Python SDK: https://github.com/arcjet/arcjet-py
Go SDK: https://github.com/arcjet/arcjet-go
## Quick start – choose your framework
Each link below directs to the SDK-scoped quick start guide for that framework:
- [Astro quick start](https://docs.arcjet.com/sdk/astro/get-started/)
- [Bun quick start](https://docs.arcjet.com/sdk/bun/get-started/)
- [Deno quick start](https://docs.arcjet.com/sdk/deno/get-started/)
- [Fastify quick start](https://docs.arcjet.com/sdk/fastify/get-started/)
- [Go SDK reference](https://docs.arcjet.com/reference/go)
- [NestJS quick start](https://docs.arcjet.com/sdk/nest/get-started/)
- [Next.js quick start](https://docs.arcjet.com/sdk/next/get-started/)
- [Node.js quick start](https://docs.arcjet.com/sdk/node/get-started/)
- [Node.js + Express quick start](https://docs.arcjet.com/sdk/node/plus/express/get-started/)
- [Node.js + Hono quick start](https://docs.arcjet.com/sdk/node/plus/hono/get-started/)
- [Nuxt quick start](https://docs.arcjet.com/sdk/nuxt/get-started/)
- [Python + FastAPI quick start](https://docs.arcjet.com/sdk/python/plus/fastapi/get-started/)
- [Python + Flask quick start](https://docs.arcjet.com/sdk/python/plus/flask/get-started/)
- [React Router quick start](https://docs.arcjet.com/sdk/react-router/get-started/)
- [Remix quick start](https://docs.arcjet.com/sdk/remix/get-started/)
- [SvelteKit quick start](https://docs.arcjet.com/sdk/sveltekit/get-started/)
- [Bun + Hono quick start](https://docs.arcjet.com/sdk/bun/plus/hono/get-started/)
Full docs: https://docs.arcjet.com
## SDK packages
| Framework | Package | Install |
| -------------- | ---------------------- | -------------------------------------- |
| Next.js | `@arcjet/next` | `npm i @arcjet/next` |
| Node.js | `@arcjet/node` | `npm i @arcjet/node` |
| Express | `@arcjet/node` | `npm i @arcjet/node` |
| Hono (Node.js) | `@arcjet/node` | `npm i @arcjet/node @hono/node-server` |
| Bun | `@arcjet/bun` | `bun add @arcjet/bun` |
| Bun + Hono | `@arcjet/bun` | `bun add @arcjet/bun hono` |
| Deno | `@arcjet/deno` | `deno add npm:@arcjet/deno` |
| Fastify | `@arcjet/fastify` | `npm i @arcjet/fastify` |
| NestJS | `@arcjet/nest` | `npm i @arcjet/nest` |
| Nuxt | `@arcjet/nuxt` | `npx nuxt module add @arcjet/nuxt` |
| Remix | `@arcjet/remix` | `npm i @arcjet/remix` |
| React Router | `@arcjet/react-router` | `npm i @arcjet/react-router` |
| SvelteKit | `@arcjet/sveltekit` | `npm i @arcjet/sveltekit` |
| Astro | `@arcjet/astro` | `npx astro add @arcjet/astro` |
| Python FastAPI | `arcjet` | `pip install arcjet` |
| Python Flask | `arcjet` | `pip install arcjet flask` |
| Go | `github.com/arcjet/arcjet-go` | `go get github.com/arcjet/arcjet-go@latest` |
## Go SDK
The Go SDK is pre-release. Version 0.1.0 requires Go 1.25 or later and supports
`net/http` request protection plus Guard protection for non-HTTP operations.
Create clients once at package scope and reuse them.
Full reference: https://docs.arcjet.com/reference/go
### Go HTTP request protection
```go
var aj = must(arcjet.NewClient(arcjet.Config{
Key: os.Getenv("ARCJET_KEY"),
Rules: []arcjet.Rule{
arcjet.Shield(arcjet.ShieldOptions{Mode: arcjet.ModeLive}),
arcjet.DetectBot(arcjet.BotOptions{
Mode: arcjet.ModeLive,
Allow: []string{},
}),
arcjet.TokenBucket(arcjet.TokenBucketOptions{
Mode: arcjet.ModeLive,
Characteristics: []string{"userId"},
RefillRate: 10,
Interval: time.Minute,
Capacity: 10,
}),
},
}))
func handler(w http.ResponseWriter, r *http.Request) {
decision, err := aj.Protect(
r.Context(),
r,
arcjet.WithCharacteristics(map[string]string{"userId": "user_123"}),
arcjet.WithRequested(1),
)
if err != nil {
// Fail-open: ERROR decision plus err. Log it and continue.
log.Printf("arcjet: %v", err)
} else if decision.IsDenied() {
status := http.StatusForbidden
if decision.Reason.IsRateLimit() {
status = http.StatusTooManyRequests
}
http.Error(w, "denied", status)
return
}
}
func must[T any](value T, err error) T {
if err != nil {
panic(err)
}
return value
}
```
Call `Protect(r.Context(), r, ...)` once inside each handler. Use
`WithCharacteristics`, `WithRequested`, `WithDetectPromptInjectionMessage`,
`WithSensitiveInfoValue`, and `WithCorrelationId` for dynamic inputs.
On a transport failure, `Protect` returns an `ERROR` conclusion `Decision`
together with `err`. `IsAllowed()` and `IsErrored()` are both true;
`IsDenied()` is false. If the client or request is nil, `Protect` returns the
zero `Decision`. Log `err` and deny only when `IsDenied()` is true. Use
`IsErrored()` to distinguish a real allow from a fail-open error.
### Go Guard protection
```go
var guard = must(arcjet.NewGuardClient(arcjet.GuardConfig{
Key: os.Getenv("ARCJET_KEY"),
}))
var promptScan = must(arcjet.GuardPromptInjection(
arcjet.GuardPromptInjectionOptions{Mode: arcjet.ModeLive}, // required
))
decision, err := guard.Guard(ctx, arcjet.GuardRequest{
Label: "tools.summarize",
CorrelationId: "trace_123",
Metadata: arcjet.Metadata{
"user": map[string]any{"id": userID},
},
Rules: []arcjet.GuardRuleInput{
promptScan.Text(prompt),
},
})
if err != nil {
return err
}
if decision.IsDenied() {
return fmt.Errorf("blocked: %s", decision.Reason)
}
if decision.HasFailedOpen() {
log.Printf("guard failed open: %+v", decision.ErrorResults())
}
```
Guard also supports rate limiting, sensitive information detection, custom
local rules, and content moderation (`GuardModerateContent`). Use `Capture`
to record what happened after a Guard call. Labels and buckets must be
lowercase slugs containing letters, digits, dashes, or dots. Standard
`HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` variables configure outbound calls.
Every Guard rule constructor requires `Mode` (`ModeLive` or `ModeDryRun`). An
empty `Mode` returns `ErrInvalidMode`. HTTP `Protect` rules default an empty
`Mode` to `ModeDryRun`. JavaScript and Python Guard rules default to `LIVE`;
Go returns a constructor error instead of defaulting to `LIVE`.
## Common setup for all frameworks
### 1. Set your key
[Create an Arcjet account](https://console.arcjet.com) then follow the
instructions to add a site and get a key. Store the key securely using
environment variables provided by your hosting platform to avoid exposing it
in source control.
Add these to your `.env.local` (Next.js), `.env` file, or environment:
```ini
ARCJET_KEY=ajkey_yourkey
ARCJET_ENV=development
```
`ARCJET_ENV=development` is required during local development so Arcjet can
correctly identify the environment. In production, set `ARCJET_ENV=production`
or omit it (defaults to production when not set).
## Next.js example
### Install
```shell
npm i @arcjet/next
```
### Configure
Create a new API route at `/app/api/arcjet/route.ts`:
```ts
import { openai } from "@ai-sdk/openai";
import arcjet, {
detectBot,
detectPromptInjection,
sensitiveInfo,
shield,
tokenBucket,
} from "@arcjet/next";
import type { UIMessage } from "ai";
import { convertToModelMessages, isTextUIPart, streamText } from "ai";
const aj = arcjet({
key: process.env.ARCJET_KEY!, // Get your site key from https://console.arcjet.com
// Track budgets per user – replace "userId" with any stable identifier
characteristics: ["userId"],
rules: [
// Shield protects against common web attacks such as SQL injection
shield({ mode: "LIVE" }),
// Block all automated clients – bots inflate AI costs
detectBot({
mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only
allow: [], // Block all bots. See https://arcjet.com/bot-list
}),
// Enforce budgets to control AI costs. Adjust rates and limits as needed.
tokenBucket({
mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only
refillRate: 2_000, // Refill 2,000 tokens per hour
interval: "1h",
capacity: 5_000, // Maximum 5,000 tokens in the bucket
}),
// Block messages containing sensitive information to prevent data leaks
sensitiveInfo({
mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only
// Block PII types that should never appear in AI prompts.
// Remove types your app legitimately handles (for example, EMAIL for a support bot).
deny: ["CREDIT_CARD_NUMBER", "EMAIL"],
}),
// Detect prompt injection attacks before they reach your AI model
detectPromptInjection({
mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only
}),
],
});
export async function POST(req: Request) {
// Replace with your session/auth lookup to get a stable user ID
const userId = "user-123";
const { messages }: { messages: UIMessage[] } = await req.json();
const modelMessages = await convertToModelMessages(messages);
// Estimate token cost: ~1 token per 4 characters of text (rough heuristic).
// For accurate counts use https://www.npmjs.com/package/tiktoken
const totalChars = modelMessages.reduce((sum, m) => {
const content =
typeof m.content === "string" ? m.content : JSON.stringify(m.content);
return sum + content.length;
}, 0);
const estimate = Math.ceil(totalChars / 4);
// Check the most recent user message for sensitive information and prompt injection.
// Pass the full conversation if you want to scan all messages.
const lastMessage: string = (messages.at(-1)?.parts ?? [])
.filter(isTextUIPart)
.map((p) => p.text)
.join(" ");
// Check with Arcjet before calling the AI provider
const decision = await aj.protect(req, {
userId,
requested: estimate,
sensitiveInfoValue: lastMessage,
detectPromptInjectionMessage: lastMessage,
});
if (decision.isDenied()) {
if (decision.reason.isBot()) {
return new Response("Automated clients are not permitted", {
status: 403,
});
} else if (decision.reason.isRateLimit()) {
return new Response("AI usage limit exceeded", { status: 429 });
} else if (decision.reason.isSensitiveInfo()) {
return new Response("Sensitive information detected", { status: 400 });
} else if (decision.reason.isPromptInjection()) {
return new Response(
"Prompt injection detected – rephrase your message",
{ status: 400 },
);
} else {
return new Response("Forbidden", { status: 403 });
}
}
const result = await streamText({
model: openai("gpt-4o"),
messages: modelMessages,
});
return result.toUIMessageStreamResponse();
}
```
The `requested` option specifies how many tokens this request consumes from the
rate limit bucket. The example estimates cost at ~1 token per 4 characters of
text. Adjust this value based on your AI provider's billing model or use a
tokenizer like `tiktoken` for accurate counts.
## Node.js + Express example
### Install
```shell
npm i @arcjet/node @arcjet/inspect express
```
### Configure
```js
// index.js
import arcjet, { shield, detectBot, tokenBucket } from "@arcjet/node";
import { isSpoofedBot } from "@arcjet/inspect";
import express from "express";
const app = express();
const port = 3000;
const aj = arcjet({
key: process.env.ARCJET_KEY,
rules: [
shield({ mode: "LIVE" }),
detectBot({
mode: "LIVE",
allow: [
"CATEGORY:SEARCH_ENGINE",
],
}),
tokenBucket({
mode: "LIVE",
refillRate: 5,
interval: 10,
capacity: 10,
}),
],
});
app.get("/", async (req, res) => {
const decision = await aj.protect(req, { requested: 5 });
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
res.writeHead(429, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Too Many Requests" }));
} else if (decision.reason.isBot()) {
res.writeHead(403, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "No bots allowed" }));
} else {
res.writeHead(403, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Forbidden" }));
}
} else {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "Hello World" }));
}
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
```
### Start
```shell
node --env-file .env index.js
```
## SvelteKit example
### Install
```shell
npm i @arcjet/sveltekit @arcjet/inspect
```
### Configure
Create a new route at `/src/routes/api/arcjet/+server.ts`:
```ts
import { env } from "$env/dynamic/private";
import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/sveltekit";
import { isSpoofedBot } from "@arcjet/inspect";
import { error, json, type RequestEvent } from "@sveltejs/kit";
const aj = arcjet({
key: env.ARCJET_KEY!,
rules: [
shield({ mode: "LIVE" }),
detectBot({
mode: "LIVE",
allow: [
"CATEGORY:SEARCH_ENGINE",
],
}),
tokenBucket({
mode: "LIVE",
refillRate: 5,
interval: 10,
capacity: 10,
}),
],
});
export async function GET(event: RequestEvent) {
const decision = await aj.protect(event, { requested: 5 });
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
return error(429, "Too Many Requests");
} else if (decision.reason.isBot()) {
return error(403, "No Bots Allowed");
} else {
return error(403, "Forbidden");
}
}
return json({ message: "Hello World" });
}
```
Note: SvelteKit passes the `event` object (not `req`) to `protect()`.
### Start
```shell
npm run dev
```
## Bun example
### Install
```shell
bun add @arcjet/bun @arcjet/inspect
```
### Configure
```ts
// index.ts
import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/bun";
import { isSpoofedBot } from "@arcjet/inspect";
import { env } from "bun";
const aj = arcjet({
key: env.ARCJET_KEY!,
rules: [
shield({ mode: "LIVE" }),
detectBot({
mode: "LIVE",
allow: [
"CATEGORY:SEARCH_ENGINE",
],
}),
tokenBucket({
mode: "LIVE",
refillRate: 5,
interval: 10,
capacity: 10,
}),
],
});
export default {
port: 3000,
fetch: aj.handler(async (req) => {
const decision = await aj.protect(req, { requested: 5 });
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
return new Response("Too many requests", { status: 429 });
} else if (decision.reason.isBot()) {
return new Response("No bots allowed", { status: 403 });
} else {
return new Response("Forbidden", { status: 403 });
}
}
return new Response("Hello world");
}),
};
```
Note: Bun uses `aj.handler()` to wrap the fetch handler, and `env` from `"bun"`
for environment variables.
### Start
```shell
bun run index.ts
```
## Deno example
### Install
```shell
deno add npm:@arcjet/deno npm:@arcjet/inspect
```
### Configure
```ts
// index.ts
import "jsr:@std/dotenv/load";
import arcjet, { detectBot, shield, tokenBucket } from "npm:@arcjet/deno";
import { isSpoofedBot } from "@arcjet/inspect";
const aj = arcjet({
key: Deno.env.get("ARCJET_KEY")!,
rules: [
shield({ mode: "LIVE" }),
detectBot({
mode: "LIVE",
allow: [
"CATEGORY:SEARCH_ENGINE",
],
}),
tokenBucket({
mode: "LIVE",
refillRate: 5,
interval: 10,
capacity: 10,
}),
],
});
Deno.serve(
{ port: 3000 },
aj.handler(async (req) => {
const decision = await aj.protect(req, { requested: 5 });
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
return new Response("Too many requests", { status: 429 });
} else if (decision.reason.isBot()) {
return new Response("No bots allowed", { status: 403 });
} else {
return new Response("Forbidden", { status: 403 });
}
}
return new Response("Hello world");
}),
);
```
Note: Deno uses `aj.handler()` to wrap the fetch handler, `Deno.env.get()` for
environment variables, and `npm:` prefix for imports.
### Start
```shell
deno run --allow-net --allow-env index.ts
```
## Fastify example
### Install
```shell
npm i @arcjet/fastify
```
### Configure
```ts
// server.ts
import Fastify from "fastify";
import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/fastify";
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
shield({ mode: "LIVE" }),
detectBot({
mode: "LIVE",
allow: [
"CATEGORY:SEARCH_ENGINE",
],
}),
tokenBucket({
mode: "LIVE",
refillRate: 5,
interval: 10,
capacity: 10,
}),
],
});
const fastify = Fastify({ logger: true });
fastify.get("/", async (request, reply) => {
const decision = await aj.protect(request, { requested: 5 });
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
return reply.status(429).send({ message: "Too many requests" });
}
if (decision.reason.isBot()) {
return reply.status(403).send({ message: "No bots allowed" });
}
return reply.status(403).send({ message: "Forbidden" });
}
return reply.status(200).send({ message: "Hello world" });
});
await fastify.listen({ port: 3000 });
```
Note: Fastify passes the `request` object (Fastify's request, not Node.js
IncomingMessage) to `protect()`.
### Start
```shell
npx tsx server.ts
```
## NestJS example
### Install
```shell
npm i @arcjet/nest
```
### Configure
Update `src/main.ts`:
```ts
import {
ArcjetGuard,
ArcjetModule,
detectBot,
fixedWindow,
shield,
} from "@arcjet/nest";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { APP_GUARD, NestFactory } from "@nestjs/core";
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
ArcjetModule.forRoot({
isGlobal: true,
key: process.env.ARCJET_KEY!,
rules: [
shield({ mode: "LIVE" }),
detectBot({
mode: "LIVE",
allow: [
"CATEGORY:SEARCH_ENGINE",
],
}),
fixedWindow({
mode: "LIVE",
window: "60s",
max: 100,
}),
],
}),
],
controllers: [],
providers: [
{
provide: APP_GUARD,
useClass: ArcjetGuard,
},
],
})
class AppModule {}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
```
Note: NestJS uses `ArcjetModule.forRoot()` for configuration and `ArcjetGuard`
as a global guard. For per-route protection, implement custom guards instead.
### Start
```shell
npm run start:dev
```
## Nuxt example
### Install
```shell
npx nuxt module add @arcjet/nuxt
```
This automatically installs and configures the Arcjet Nuxt integration.
### Configure
Create a server route at `server/api/protected.get.ts`:
```ts
import arcjetNuxt, { detectBot, shield, tokenBucket } from "#arcjet";
const arcjet = arcjetNuxt({
rules: [
shield({ mode: "LIVE" }),
detectBot({
mode: "LIVE",
allow: [
"CATEGORY:SEARCH_ENGINE",
],
}),
tokenBucket({
mode: "LIVE",
refillRate: 5,
interval: 10,
capacity: 10,
}),
],
});
export default defineEventHandler(async (event) => {
const decision = await arcjet.protect(event, { requested: 5 });
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
throw createError({
statusCode: 429,
statusMessage: "Too Many Requests",
});
}
if (decision.reason.isBot()) {
throw createError({
statusCode: 403,
statusMessage: "No bots allowed",
});
}
throw createError({
statusCode: 403,
statusMessage: "Forbidden",
});
}
return { message: "Hello world" };
});
```
Note: Nuxt imports from the `#arcjet` virtual module (not a package name). The
ARCJET_KEY is set in your `nuxt.config.ts` via the module options. Nuxt passes
the `event` object to `protect()`.
### Start
```shell
npm run dev
```
## Remix example
### Install
```shell
npm i @arcjet/remix @arcjet/inspect
```
### Configure
Create a route at `app/routes/arcjet.tsx`:
```tsx
import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/remix";
import type { LoaderFunctionArgs } from "@remix-run/node";
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
shield({ mode: "LIVE" }),
detectBot({
mode: "LIVE",
allow: [
"CATEGORY:SEARCH_ENGINE",
],
}),
tokenBucket({
mode: "LIVE",
refillRate: 5,
interval: 10,
capacity: 10,
}),
],
});
export async function loader(args: LoaderFunctionArgs) {
const decision = await aj.protect(args, { requested: 5 });
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
throw new Response("Too many requests", { status: 429 });
} else if (decision.reason.isBot()) {
throw new Response("Bots forbidden", { status: 403 });
} else {
throw new Response("Forbidden", { status: 403 });
}
}
return null;
}
export default function Index() {
return
Hello world
;
}
```
Note: Remix passes the `args` (LoaderFunctionArgs or ActionFunctionArgs) to
`protect()`.
### Start
```shell
npm run dev
```
## React Router example
### Install
```shell
npm i @arcjet/react-router @arcjet/inspect
```
### Configure
Create a route at `app/routes/home.tsx`:
```tsx
import arcjetReactRouter, {
detectBot,
shield,
tokenBucket,
} from "@arcjet/react-router";
import type { Route } from "../routes/+types/home";
const arcjet = arcjetReactRouter({
key: process.env.ARCJET_KEY!,
rules: [
detectBot({
mode: "LIVE",
allow: [
"CATEGORY:SEARCH_ENGINE",
],
}),
shield({ mode: "LIVE" }),
tokenBucket({
mode: "LIVE",
capacity: 10,
interval: 10,
refillRate: 5,
}),
],
});
export async function loader(args: Route.LoaderArgs) {
const decision = await arcjet.protect(args, { requested: 5 });
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
throw new Response("Too many requests", { status: 429 });
} else if (decision.reason.isBot()) {
throw new Response("Bots forbidden", { status: 403 });
} else {
throw new Response("Forbidden", { status: 403 });
}
}
return undefined;
}
export default function Home() {
return Hello world
;
}
```
Note: React Router passes loader/action `args` to `protect()`.
### Start
```shell
npm run dev
```
## Astro example
### Install
```shell
npx astro add @arcjet/astro
```
### Configure
Update `astro.config.mjs`:
```js
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
import arcjet, { shield, detectBot, tokenBucket } from "@arcjet/astro";
export default defineConfig({
adapter: node({ mode: "standalone" }),
integrations: [
arcjet({
rules: [
shield({ mode: "LIVE" }),
detectBot({
mode: "LIVE",
allow: [
"CATEGORY:SEARCH_ENGINE",
],
}),
tokenBucket({
mode: "LIVE",
refillRate: 5,
interval: 10,
capacity: 10,
}),
],
}),
],
});
```
Create an API route at `src/pages/api.json.ts`:
```ts
export const prerender = false;
import type { APIRoute } from "astro";
import aj from "arcjet:client";
export const GET: APIRoute = async ({ request }) => {
const decision = await aj.protect(request, { requested: 5 });
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
return Response.json({ error: "Too Many Requests" }, { status: 429 });
} else if (decision.reason.isBot()) {
return Response.json({ error: "No bots allowed" }, { status: 403 });
} else {
return Response.json({ error: "Forbidden" }, { status: 403 });
}
}
return Response.json({ message: "Hello world" });
};
```
Note: Astro imports the Arcjet client from the `arcjet:client` virtual module.
The ARCJET_KEY is set in your environment variables. The Astro adapter must be
configured for server-side rendering.
### Start
```shell
npm run dev
```
## Node.js + Hono example
### Install
```shell
npm i @arcjet/node @arcjet/inspect @hono/node-server hono
```
### Configure
```ts
// index.ts
import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/node";
import { serve, type HttpBindings } from "@hono/node-server";
import { Hono } from "hono";
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
shield({ mode: "LIVE" }),
detectBot({
mode: "LIVE",
allow: ["CATEGORY:SEARCH_ENGINE"],
}),
tokenBucket({
mode: "LIVE",
refillRate: 5,
interval: 10,
capacity: 10,
}),
],
});
const app = new Hono<{ Bindings: HttpBindings }>();
app.get("/", async (c) => {
const decision = await aj.protect(c.env.incoming, { requested: 5 });
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
return c.json({ error: "Too Many Requests" }, 429);
} else if (decision.reason.isBot()) {
return c.json({ error: "No Bots Allowed" }, 403);
} else {
return c.json({ error: "Forbidden" }, 403);
}
}
return c.json({ message: "Hello Hono!" });
});
serve({ fetch: app.fetch, port: 3000 });
```
Note: With Hono on Node.js, pass `c.env.incoming` (the Node.js IncomingMessage)
to `protect()`, not `c.req`.
## Bun + Hono example
### Install
```shell
bun add @arcjet/bun @arcjet/inspect hono
```
### Configure
```ts
// index.ts
import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/bun";
import { Hono } from "hono";
import { env } from "bun";
const aj = arcjet({
key: env.ARCJET_KEY!,
rules: [
shield({ mode: "LIVE" }),
detectBot({
mode: "LIVE",
allow: ["CATEGORY:SEARCH_ENGINE"],
}),
tokenBucket({
mode: "LIVE",
refillRate: 5,
interval: 10,
capacity: 10,
}),
],
});
const app = new Hono();
app.get("/", async (c) => {
const decision = await aj.protect(c.req.raw, { requested: 5 });
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
return c.json({ error: "Too many requests" }, 429);
} else if (decision.reason.isBot()) {
return c.json({ error: "No bots allowed" }, 403);
} else {
return c.json({ error: "Forbidden" }, 403);
}
}
return c.json({ message: "Hello world" });
});
export default {
fetch: aj.handler(app.fetch),
port: 3000,
};
```
Note: With Hono on Bun, pass `c.req.raw` (the raw Request) to `protect()`, and
wrap the fetch handler with `aj.handler()`.
## Python FastAPI example
### Install
```shell
pip install arcjet fastapi uvicorn
# or with uv:
uv add arcjet fastapi uvicorn
```
### Configure
```python
# main.py
import os
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from arcjet import (
Mode,
SensitiveInfoEntityType,
arcjet,
detect_bot,
detect_prompt_injection,
detect_sensitive_info,
shield,
token_bucket,
)
app = FastAPI()
aj = arcjet(
key=os.getenv("ARCJET_KEY"), # Get your key from https://console.arcjet.com
rules=[
# Detect prompt injection attacks before they reach your LLM
detect_prompt_injection(mode=Mode.LIVE),
# Block sensitive data (PII, credit cards) from entering your AI pipeline
detect_sensitive_info(
mode=Mode.LIVE,
deny=[
SensitiveInfoEntityType.CREDIT_CARD_NUMBER,
SensitiveInfoEntityType.EMAIL,
SensitiveInfoEntityType.PHONE_NUMBER,
],
),
# Rate limit by token budget per user
token_bucket(
characteristics=["userId"],
mode=Mode.LIVE,
refill_rate=100,
interval=60,
capacity=1000,
),
# Block automated clients and scrapers
detect_bot(
mode=Mode.LIVE,
allow=[], # empty = block all bots
),
# Protect against common web attacks (SQLi, XSS, etc.)
shield(mode=Mode.LIVE),
],
)
class ChatRequest(BaseModel):
message: str
@app.post("/chat")
async def chat(request: Request, body: ChatRequest):
userId = "user_123" # Replace with real user ID from session
decision = await aj.protect(
request,
requested=5,
characteristics={"userId": userId},
detect_prompt_injection_message=body.message,
sensitive_info_value=body.message,
)
if decision.is_denied():
status = 429 if decision.reason_v2.type == "RATE_LIMIT" else 403
return JSONResponse({"error": "Denied"}, status_code=status)
# Safe to pass body.message to your LLM
return {"reply": "Hello!"}
```
### Start
```shell
uvicorn main:app --reload
```
## Python Flask example
### Install
```shell
pip install arcjet flask
# or with uv:
uv add arcjet flask
```
### Configure
```python
# main.py
import os
from flask import Flask, jsonify, request
from arcjet import (
Mode,
SensitiveInfoEntityType,
arcjet_sync,
detect_bot,
detect_prompt_injection,
detect_sensitive_info,
shield,
token_bucket,
)
app = Flask(__name__)
aj = arcjet_sync(
key=os.getenv("ARCJET_KEY"), # Get your key from https://console.arcjet.com
rules=[
# Detect prompt injection attacks before they reach your LLM
detect_prompt_injection(mode=Mode.LIVE),
# Block sensitive data (PII, credit cards) from entering your AI pipeline
detect_sensitive_info(
mode=Mode.LIVE,
deny=[
SensitiveInfoEntityType.CREDIT_CARD_NUMBER,
SensitiveInfoEntityType.EMAIL,
SensitiveInfoEntityType.PHONE_NUMBER,
],
),
# Rate limit by token budget per user
token_bucket(
characteristics=["userId"],
mode=Mode.LIVE,
refill_rate=100,
interval=60,
capacity=1000,
),
# Block automated clients and scrapers
detect_bot(
mode=Mode.LIVE,
allow=[], # empty = block all bots
),
# Protect against common web attacks (SQLi, XSS, etc.)
shield(mode=Mode.LIVE),
],
)
@app.post("/chat")
def chat():
userId = "user_123" # Replace with real user ID from session
body = request.get_json()
message = body.get("message", "") if body else ""
decision = aj.protect(
request,
requested=5,
characteristics={"userId": userId},
detect_prompt_injection_message=message,
sensitive_info_value=message,
)
if decision.is_denied():
status = 429 if decision.reason_v2.type == "RATE_LIMIT" else 403
return jsonify(error="Denied"), status
# Safe to pass message to your LLM
return jsonify(reply="Hello!")
if __name__ == "__main__":
app.run(debug=True)
```
Note: Flask uses `arcjet_sync` (synchronous) instead of `arcjet` (async).
### Start
```shell
flask run
# or: uv run flask run
```
## Rule parameter reference
Every rule accepts `mode: "LIVE" | "DRY_RUN"`. In `DRY_RUN` mode the rule
evaluates and returns a decision but never blocks. Use `DRY_RUN` for testing.
In Python, HTTP rule factories require `mode`. Omitting it raises
`TypeError`. Guard constructors default to `Mode.LIVE`. JavaScript HTTP
rules default to `"DRY_RUN"`.
### shield(options)
Protects against common web attacks, including SQL injection and XSS.
```ts
shield({
mode: "LIVE", // or "DRY_RUN"
})
```
Parameters:
- `mode` (optional): `"LIVE"` (default) or `"DRY_RUN"`
Python: `shield(mode=Mode.LIVE)`
### detectBot(options)
Detects and blocks automated clients.
```ts
detectBot({
mode: "LIVE",
// Use allow OR deny (mutually exclusive)
allow: [
"CATEGORY:SEARCH_ENGINE", // Google, Bing, etc
"CATEGORY:MONITOR", // Uptime monitoring
"CATEGORY:PREVIEW", // Link previews (Slack, Discord)
// Or specific bots: "GOOGLEBOT", "BINGBOT", etc.
],
// OR:
// deny: ["CATEGORY:DEFINITELY_AUTOMATED"],
})
```
Parameters:
- `mode` (optional): `"LIVE"` or `"DRY_RUN"`
- `allow` (array): Bots/categories to allow – everything else is denied
- `deny` (array): Bots/categories to deny – everything else is allowed
- `allow` and `deny` are mutually exclusive; use one or the other
Bot categories use the `CATEGORY:` prefix. Full list: https://arcjet.com/bot-list
Python: `detect_bot(mode=Mode.LIVE, allow=[BotCategory.SEARCH_ENGINE])` or
`detect_bot(mode=Mode.LIVE, allow=["CURL"])`. Use `BotCategory.` for
categories or pass specific bot name strings directly. Pass exactly one of
`allow` or `deny`. `allow=[]` blocks every detected bot. Passing neither list
or both lists raises `ValueError`.
### tokenBucket(options)
Token bucket rate limiting. Tokens refill at a steady rate. Best for AI cost
control where each request consumes a variable number of tokens.
```ts
tokenBucket({
mode: "LIVE",
characteristics: ["userId"], // Optional. Defaults to IP-based tracking
refillRate: 2_000, // Tokens added per interval
interval: "1h", // Refill interval (number in seconds, or string: "1s", "1m", "1h", "1d")
capacity: 5_000, // Maximum tokens the bucket can hold
})
```
At protect() time, pass `requested` to deduct tokens:
```ts
const decision = await aj.protect(req, { requested: 50 });
```
Parameters:
- `mode` (optional): `"LIVE"` or `"DRY_RUN"`
- `characteristics` (optional): Array of strings for tracking (default: IP)
- `refillRate` (required): Number of tokens to add per interval
- `interval` (required): Seconds (number) or duration string (`"1h"`, `"10m"`)
- `capacity` (required): Maximum tokens in the bucket
Python: `token_bucket(mode=Mode.LIVE, refill_rate=100, interval=60, capacity=1000, characteristics=["userId"])`
The `interval` parameter accepts seconds as a number in Python.
### fixedWindow(options)
Fixed window rate limiting. Counts requests in non-overlapping time windows.
```ts
fixedWindow({
mode: "LIVE",
characteristics: ["userId"], // Optional
window: "60s", // Window duration (string: "1s", "10s", "1m", "1h", "1d")
max: 100, // Maximum requests per window
})
```
Parameters:
- `mode` (optional): `"LIVE"` or `"DRY_RUN"`
- `characteristics` (optional): Array of strings for tracking (default: IP)
- `window` (required): Duration string, such as `"60s"` or `"1h"`
- `max` (required): Maximum requests allowed per window
Python: `fixed_window(mode=Mode.LIVE, window=60, max=100)` – `window` takes
seconds as a number in Python.
### slidingWindow(options)
Sliding window rate limiting. Smooths out the edges of fixed windows.
```ts
slidingWindow({
mode: "LIVE",
characteristics: ["userId"], // Optional
interval: 60, // Window size in seconds
max: 100, // Maximum requests per window
})
```
Parameters:
- `mode` (optional): `"LIVE"` or `"DRY_RUN"`
- `characteristics` (optional): Array of strings for tracking (default: IP)
- `interval` (required): Window size in seconds (number)
- `max` (required): Maximum requests allowed per window
Python: `sliding_window(mode=Mode.LIVE, interval=60, max=100)` – `interval`
takes seconds as a number.
Rate-limit characteristics are combined into one fingerprint. For example,
`["ip.src", "userId"]` creates one bucket for each unique IP/user pair, not
separate IP and user counters. Configure separate rate-limit rules when you
need independent limits, such as one rule keyed by `userId` for an account
quota and another keyed by `ip.src` for per-IP throttling. IP-based limits are
useful for anonymous traffic, but IP addresses can be shared or rotated; use a
stable user, account, tenant, or API key for authenticated traffic.
### sensitiveInfo(options)
Detects and blocks requests containing sensitive information (PII).
```ts
sensitiveInfo({
mode: "LIVE",
// Use allow OR deny (mutually exclusive)
deny: ["CREDIT_CARD_NUMBER", "EMAIL", "PHONE_NUMBER", "IP_ADDRESS"],
// OR: allow: ["EMAIL"], // Only allow email, block everything else
})
```
At protect() time, pass the text to scan:
```ts
const decision = await aj.protect(req, {
sensitiveInfoValue: "text to scan for PII",
});
```
Parameters:
- `mode` (optional): `"LIVE"` or `"DRY_RUN"`
- `deny` (array): Entity types to block
- `allow` (array): Entity types to allow (blocks everything else)
- `deny` and `allow` are mutually exclusive
- `contextWindowSize` (optional): Number of tokens for detection context (default: 1)
- `detect` (optional): Custom detection function `(tokens: string[]) => Array`
- `backend` (optional): Alternative detection backend. Defaults to the built-in
WebAssembly engine; pass the on-device Rampart NER model to also detect names,
addresses, and government/financial identifiers. All detection stays local.
Entity types detected by the built-in engine: `CREDIT_CARD_NUMBER`, `EMAIL`,
`PHONE_NUMBER`, `IP_ADDRESS`. The optional Rampart backend adds `GIVEN_NAME`,
`SURNAME`, `SSN`, `URL`, `TAX_ID`, `BANK_ACCOUNT`, `ROUTING_NUMBER`,
`GOVERNMENT_ID`, `PASSPORT`, `DRIVERS_LICENSE`, and address parts (`BUILDING_NUMBER`,
`STREET_NAME`, `SECONDARY_ADDRESS`, `CITY`, `STATE`, `ZIP_CODE`).
Rampart is an optional package: `@arcjet/sensitive-info-rampart` for the
JavaScript SDKs, or the `arcjet[sensitive-info-rampart]` extra for Python. Import
`rampart` and pass it as `backend: rampart()` (JS) / `backend=rampart()` (Python).
It loads a bundled ONNX model on first use and needs a server runtime (Node.js,
Bun, or Deno in JS). Full reference:
https://docs.arcjet.com/sensitive-info/reference#on-device-detection-with-rampart
Python: `detect_sensitive_info(mode=Mode.LIVE, deny=[SensitiveInfoEntityType.EMAIL, SensitiveInfoEntityType.CREDIT_CARD_NUMBER])`
At protect() time: `sensitive_info_value="text to scan"`
### detectPromptInjection(options)
Detects prompt injection attacks in user messages before they reach your AI model.
```ts
detectPromptInjection({
mode: "LIVE",
})
```
At protect() time, pass the message to scan:
```ts
const decision = await aj.protect(req, {
detectPromptInjectionMessage: userMessage,
});
```
Parameters:
- `mode` (optional): `"LIVE"` or `"DRY_RUN"`
JavaScript accepts only `mode`. `threshold` and `score` are removed on JS SDK
`main` – drop them on upgrade. The core SDK ignores leftover `threshold`.
`@arcjet/astro` Zod `.strict()` throws at startup if `threshold` is still in
the integration config.
The verdict is binary: `decision.reason.isPromptInjection()` or
`decision.reason.injectionDetected`.
Python: `detect_prompt_injection(mode=Mode.LIVE)` with
`detect_prompt_injection_message=message` at protect() time. `mode` is
required: omitting it or passing `threshold=` raises `TypeError`. Drop
`threshold`. `PromptInjectionReason.score` remains deprecated. Guard
`DetectPromptInjection` defaults to `LIVE`.
### validateEmail(options)
Validates email addresses for signup forms.
```ts
validateEmail({
mode: "LIVE",
deny: ["DISPOSABLE", "NO_MX_RECORDS", "INVALID"],
// OR: allow: ["FREE"],
})
```
At protect() time, pass the email:
```ts
const decision = await aj.protect(req, { email: "user@example.com" });
```
Parameters:
- `mode` (optional): `"LIVE"` or `"DRY_RUN"`
- `deny` (array): Email types to reject
- `allow` (array): Email types to allow (rejects everything else)
- `requireTopLevelDomain` (optional): Require a TLD (default: `true`)
- `allowDomainLiteral` (optional): Allow domain literals like `[127.0.0.1]` (default: `false`)
Valid email types: `DISPOSABLE`, `FREE`, `NO_MX_RECORDS`, `NO_GRAVATAR`, `INVALID`
Python: `validate_email(mode=Mode.LIVE, deny=[EmailType.DISPOSABLE, EmailType.INVALID, EmailType.NO_MX_RECORDS])`
At protect() time: `email="user@example.com"`. Pass exactly one of `allow` or
`deny`. `allow=[]` allows no email types. Passing neither list or both lists
raises `ValueError`.
### protectSignup(options)
Combined rule for signup form protection (bot detection + email validation + rate limiting).
```ts
protectSignup({
email: {
mode: "LIVE",
deny: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"],
},
bots: {
mode: "LIVE",
deny: ["CATEGORY:DEFINITELY_AUTOMATED"],
},
rateLimit: {
mode: "LIVE",
characteristics: ["ip.src"],
interval: 600,
max: 5,
},
})
```
At protect() time, pass the email:
```ts
const decision = await aj.protect(req, { email: "user@example.com" });
```
Python: `protect_signup` is not one composite rule. It returns a
`(SlidingWindow, BotDetection, EmailValidation)` tuple that you unpack into
`rules`. All three mappings are required (JS `ProtectSignupOptions` fields are
optional). Nested `bots` and `email` must include exactly one of `allow` or
`deny` (`allow=[]` is valid). Nested mappings are forwarded to
`sliding_window()`, `detect_bot()`, and `validate_email()`, so each mapping
must include `mode`.
```py
import os
from arcjet import EmailType, Mode, arcjet, protect_signup
aj = arcjet(
key=os.environ["ARCJET_KEY"],
rules=[
*protect_signup(
rate_limit={"mode": Mode.LIVE, "max": 5, "interval": 600},
bots={"mode": Mode.LIVE, "allow": []},
email={
"mode": Mode.LIVE,
"deny": [
EmailType.DISPOSABLE,
EmailType.INVALID,
EmailType.NO_MX_RECORDS,
],
},
)
],
)
```
At protect() time: `email="user@example.com"`.
### filter(options)
Filter requests based on expressions using request and IP metadata.
```ts
filter({
mode: "LIVE",
// Use allow OR deny (mutually exclusive)
deny: ["ip.src.vpn", "ip.src.tor"],
// OR: allow: ['ip.src.country eq "US"'],
})
```
Parameters:
- `mode` (optional): `"LIVE"` or `"DRY_RUN"`
- `deny` (array): Expressions that cause a DENY when matched
- `allow` (array): Expressions that cause an ALLOW when matched (denies everything else)
- Maximum 10 expressions per rule, each max 1024 bytes
Available fields include: `http.host`, `http.request.method`,
`http.request.uri.path`, `ip.src`, `ip.src.country`, `ip.src.vpn`,
`ip.src.tor`, `ip.src.hosting`, `ip.src.proxy`, and many more.
Python: `filter_request(mode=Mode.LIVE, deny=["ip.src.vpn", "ip.src.tor"])`
Custom local fields: pass `filter_local={"key": "value"}` at protect() time,
then reference as `local.key` in expressions.
## Decision API reference
`protect()` returns a decision object with these methods:
### Conclusion methods
```ts
const decision = await aj.protect(req);
decision.isDenied() // true if any LIVE rule triggered a DENY
decision.isAllowed() // true if all rules passed
decision.isErrored() // true if there was an error evaluating rules
decision.isChallenged() // true if a challenge is required
```
### Reason methods (check WHY a request was denied)
```ts
if (decision.isDenied()) {
decision.reason.isRateLimit() // Rate limit exceeded
decision.reason.isBot() // Bot detected
decision.reason.isShield() // Shield WAF triggered
decision.reason.isSensitiveInfo() // PII detected
decision.reason.isEmail() // Email validation failed
decision.reason.isPromptInjection() // Prompt injection detected
decision.reason.injectionDetected // Binary prompt-injection verdict
decision.reason.isFilterRule() // Filter rule matched
}
```
### Error handling
```ts
if (decision.isErrored()) {
// Arcjet fails open – log the error and allow the request
console.error("Arcjet error", decision.reason.message);
}
```
### IP analysis (available on every decision)
```ts
decision.ip.isHosting() // true if from a hosting/cloud provider
decision.ip.isVpn() // true if from a VPN
decision.ip.isTor() // true if from Tor
decision.ip.isProxy() // true if from a proxy
decision.ip.isRelay() // true if from a relay
```
### Rate limit metadata
```ts
// Available when a rate limit rule is configured
for (const result of decision.results) {
if (result.reason.isRateLimit()) {
result.reason.max // Configured maximum
result.reason.remaining // Requests/tokens remaining
result.reason.window // Total window in seconds
result.reason.reset // Seconds until window resets
}
}
```
### Python decision API
```python
# Top-level checks
decision.is_denied() # True if any rule denied the request
decision.is_allowed() # True if all rules allowed the request
decision.is_error() # True if Arcjet encountered an error (fails open)
# reason_v2.type values: "BOT", "RATE_LIMIT", "SHIELD", "EMAIL", "ERROR", "FILTER"
if decision.reason_v2.type == "RATE_LIMIT":
print(decision.reason_v2.remaining) # tokens/requests remaining
elif decision.reason_v2.type == "BOT":
print(decision.reason_v2.denied) # list of denied bot names
# Per-rule results (for granular handling)
for result in decision.results:
print(result.reason_v2.type, result.is_denied())
# Inspect helpers (from arcjet; no extra package). DRY_RUN is ignored by
# is_verified_bot, is_spoofed_bot, and is_missing_user_agent.
from arcjet import (
is_missing_user_agent,
is_spoofed_bot,
is_verified_bot,
set_rate_limit_headers,
)
if any(is_verified_bot(r) for r in decision.results):
print("verified bot")
if any(is_spoofed_bot(r) for r in decision.results):
print("spoofed bot")
if any(is_missing_user_agent(r) for r in decision.results):
print("missing User-Agent")
# IETF RateLimit / RateLimit-Policy headers (same as JS @arcjet/decorate)
set_rate_limit_headers(response, decision)
# IP helpers (same as JS)
decision.ip.is_hosting()
decision.ip.is_vpn()
decision.ip.is_tor()
decision.ip.is_proxy()
```
### Python protect() parameters
All parameters are optional keyword arguments passed alongside `request`:
| Parameter | Type | Used by |
| --------------------------------- | ---------------- | -------------------------- |
| `requested` | `int` | Token bucket rate limit |
| `characteristics` | `dict[str, Any]` | Rate limiting |
| `detect_prompt_injection_message` | `str` | Prompt injection detection |
| `sensitive_info_value` | `str` | Sensitive info detection |
| `email` | `str` | Email validation |
| `filter_local` | `dict[str, str]` | Request filters |
| `ip_src` | `str` | Manual IP override |
## withRule() pattern – reusing a single client
Create one Arcjet instance and add route-specific rules with `withRule()`
(JS) or `with_rule()` (Python). The Python clone shares `DecisionCache`,
key, characteristics, and transport. The original client is unchanged.
`with_rule()` accepts a single rule or a sequence of rules. HTTP Python
rule factories require `mode`.
```ts
// lib/arcjet.ts – create and export a base instance
import arcjet, {
detectBot,
fixedWindow,
sensitiveInfo,
shield,
} from "@arcjet/next";
export { detectBot, fixedWindow, sensitiveInfo, shield };
export default arcjet({
key: process.env.ARCJET_KEY!,
rules: [
// Base rules that apply to every route (optional)
],
});
```
```ts
// app/api/chat/route.ts – add route-specific rules
import arcjet, { detectBot, fixedWindow } from "@/lib/arcjet";
const aj = arcjet
.withRule(
detectBot({
mode: "LIVE",
allow: [],
}),
)
.withRule(
fixedWindow({
mode: "LIVE",
max: 100,
window: "60s",
}),
);
export async function GET(req: Request) {
const decision = await aj.protect(req);
// ...
}
```
Python:
```py
import os
from arcjet import Mode, arcjet, detect_bot, fixed_window, shield
aj = arcjet(key=os.environ["ARCJET_KEY"], rules=[shield(mode=Mode.LIVE)])
protected = aj.with_rule(
[
detect_bot(mode=Mode.LIVE, allow=[]),
fixed_window(mode=Mode.LIVE, window=60, max=100),
]
)
```
## Best practices and anti-patterns
### Do
- Create the Arcjet client ONCE, outside the request handler, and reuse it.
- Call `protect()` inside the route handler where you have the full request
context.
- Start new rules in `DRY_RUN` mode, verify in the Console, then switch to
`LIVE`.
- Handle every denial reason explicitly, including rate limit, bot, and shield.
- Use `withRule()` (JS) or `with_rule()` (Python) to attach route-specific
rules to a shared base instance.
### Don't
- Don't create a new Arcjet instance per request – this defeats caching.
- Don't call `protect()` multiple times for the same request – it may double-
count rate limits.
- Don't use Arcjet in middleware – middleware lacks route context. Call
`protect()` in each route handler instead. If you must use middleware, scope
it carefully and don't also call `protect()` in the route handler.
- Don't pass personal information (email addresses, names) as rate limit
`characteristics` – use opaque identifiers such as user IDs.
- Next.js renamed `middleware.js` to `proxy.js` in Next.js 16. Call the Arcjet
`protect()` function only once per request.
### Proxies and load balancers
If your app is behind a proxy or load balancer, configure Arcjet to see the
real client IP:
```ts
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [],
proxies: [
"203.0.113.100", // A single IP
"203.0.113.0/24", // A CIDR range
],
});
```
This is not needed on Firebase, Netlify, Fly.io, or Vercel – Arcjet
auto-detects proxy IPs on these platforms.
## Product philosophy
1. Enforcement runs inline in your application – with access to identity,
route, session, and spend context no proxy can see.
2. Your agent configures protections via MCP. You review and approve.
3. Start in DRY_RUN, verify against real traffic, promote to LIVE.
4. Remote rules let you and your agents respond to attacks immediately – no code
deployment needed.
Find out more at https://docs.arcjet.com/architecture
## Important notes
- Arcjet runs server-side and does not require any client-side integration.
- Arcjet is a paid service. See https://arcjet.com/pricing for details.
- Review https://docs.arcjet.com/best-practices for best practices.
- Calls to `protect()` never throw. Arcjet fails open so that a service issue
or misconfiguration does not block all requests.
## Guards – non-request protection
Guards apply Arcjet security rules inside AI agent tool calls, MCP tool
handlers, queue workers, and anywhere else you process untrusted input without an
HTTP request. Pass inputs directly, get a decision back.
Supported languages: **JavaScript / TypeScript** (`@arcjet/guard`), **Python** (the `arcjet` package), and **Go** (`arcjet-go`, pre-release).
### JavaScript / TypeScript example
```ts
import { launchArcjet, tokenBucket, detectPromptInjection } from "@arcjet/guard";
// Create once at module scope
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
// Configure rules at module scope (stable IDs for server-side aggregation)
const userLimit = tokenBucket({
label: "user.tool_call_bucket",
bucket: "tool-calls",
refillRate: 100,
intervalSeconds: 60,
maxTokens: 500,
});
const piRule = detectPromptInjection();
// Call guard() inline in each tool handler
async function searchWeb(query: string, userId: string) {
const decision = await arcjet.guard({
label: "tools.search_web",
metadata: { userId },
rules: [
userLimit({ key: userId, requested: 1 }),
piRule(query),
],
});
if (decision.conclusion === "DENY") {
const rateDenied = userLimit.deniedResult(decision);
if (rateDenied) {
throw new Error(`Rate limited -- try again in ${rateDenied.resetInSeconds}s`);
}
throw new Error(`Blocked: ${decision.reason}`);
}
// Safe to proceed
}
```
### Python example
```python
import os
from arcjet.guard import launch_arcjet, TokenBucket, DetectPromptInjection
# Create once at module scope
arcjet = launch_arcjet(key=os.environ["ARCJET_KEY"])
# Configure rules at module scope (stable IDs for server-side aggregation)
user_limit = TokenBucket(
label="user.tool_call_bucket",
bucket="tool-calls",
refill_rate=100,
interval_seconds=60,
max_tokens=500,
)
pi_rule = DetectPromptInjection()
# Call guard() inline in each tool handler
async def search_web(query: str, user_id: str):
decision = await arcjet.guard(
label="tools.search_web",
metadata={"user_id": user_id},
rules=[
user_limit(key=user_id, requested=1),
pi_rule(query),
],
)
if decision.conclusion == "DENY":
rate_denied = user_limit.denied_result(decision)
if rate_denied:
raise RuntimeError(f"Rate limited -- try again in {rate_denied.reset_in_seconds}s")
raise RuntimeError(f"Blocked: {decision.reason}")
# Safe to proceed
```
The guard skill is the source of truth for code patterns:
```bash
npx skills add arcjet/skills
```
For the full API reference, read the installed library source:
- JS/TS: `node_modules/@arcjet/guard`
- Python: `arcjet.guard` module
### Agent framework adapters
The examples above call `guard()` directly. When the agent framework owns the
tool loop, use its adapter instead: the wrapper sits between the model's
generated arguments and the tool's own handler, so a denial stops the side
effect and returns an envelope the model can read. Framework wrappers take
`action`; direct `guard()` calls take `label` for the same slug.
Every adapter page below documents one integration and selects the language
with a tab where both a JavaScript and a Python adapter exist.
| Framework | JavaScript import | Python import | Deny point |
| --- | --- | --- | --- |
| Vercel AI SDK | `@arcjet/guard/vercel-ai/v7` | – | `guardTool`, `guardAction` |
| LangChain | `@arcjet/guard/langchain/v1` | `arcjet.guard.langchain` | `guardTool` / `guard_tool`, `guardMiddleware` / `ArcjetMiddleware` |
| LangGraph | `@arcjet/guard/langgraph/v1` | – | `guardTool`, `guardToolNode` |
| CrewAI | – | `arcjet.guard.crewai` | `register_arcjet_hooks` on `PRE_TOOL_CALL`, `guard_tool` |
| Genkit | `@arcjet/guard/genkit/v1` | – | `guardTool`, `guardMiddleware` |
| Google ADK | `@arcjet/guard/google-adk/v2` | – | `guardPlugin` (`beforeToolCallback`). No `guardTool` |
| OpenAI Agents | `@arcjet/guard/openai-agents/v0` | `arcjet.guard.openai_agents` | `guardTool` on `invoke` / `guard_tool` on `tool_input_guardrails` |
| Strands Agents | `@arcjet/guard/strands-agents/v1` | `arcjet.guard.strands_agents` | `guardTool` / `guard_tool`, `guardHooks` / `guard_hooks` |
| TanStack AI | `@arcjet/guard/tanstack-ai/v0` | – | `guardMiddleware` (`onBeforeToolCall`). No `guardTool` |
| Mastra | `@arcjet/guard/mastra/v1` | – | `guardProcessor`, `guardTool`, `guardHooks` |
| Vercel Eve | `@arcjet/guard/vercel-eve/v0` | – | `guardInbound`, `guardTool`, `guardApproval` (connections) |
| Claude Agent SDK | `@arcjet/guard/claude-agent-sdk/v0` | `arcjet.guard.claude_agent_sdk` | `guardTool` / `guard_tool`, `guardHooks` / `guard_hooks` (`UserPromptSubmit`, `PreToolUse`) |
| Claude Managed Agents | `@arcjet/guard/claude-managed-agents/v0` | `arcjet.guard.claude_managed_agents` | `guardEvents` / `guard_events`, `guardCustomTool` / `guard_custom_tool` |
Every JavaScript path is versioned. Unversioned aliases such as
`@arcjet/guard/vercel-ai` do not resolve. Don't wrap the same tool with two
adapters, and don't mix the JavaScript and Python adapter for one framework.
A framework's human-in-the-loop confirmation is not a policy gate.
`needsApproval`, `humanInTheLoopMiddleware`, `interrupt()`, `requireApproval`,
`human_input`, `can_use_tool`, and `always_ask` all pause a run for a person.
The runtime can skip some of them, and none of them evaluates a policy. Don't
put Arcjet policy on any of them.
The one exception is `guardApproval` on `@arcjet/guard/vercel-eve/v0`, which
evaluates a policy on an Eve connection's `approval` field. A connection's
tools have no local handler to wrap, so this is the only enforcement point
that reaches them. `onAllow: "user-approval"` still requires a person after
the policy passes. No other adapter has an approval helper.
The wrappers fail closed: if Guard cannot be evaluated the tool does not run.
Direct `guard()` fails open and reports `hasFailedOpen()` /
`has_failed_open()`, so an `ALLOW` from a direct call is not proof the rules
ran.
### Common mistakes when writing Guard code
These are the traps that produce code which looks correct, runs without an
error, and enforces nothing.
**Configure the local sensitive-information rule.** In Python,
`LocalDetectSensitiveInfo()` with neither `allow` nor `deny` fails during
local evaluation. The rule result is
`RuleResultError(conclusion='ALLOW', reason='ERROR', code='AJ1203')` and the
decision conclusion is `ALLOW`, so the check looks configured and blocks
nothing. Only `has_failed_open()` reveals it. JavaScript
`localDetectSensitiveInfo()` does work with no arguments. Always pass an
explicit list in both languages.
**The rule needs its own `backend`.** The default WASM backend detects
`EMAIL`, `PHONE_NUMBER`, `IP_ADDRESS`, and `CREDIT_CARD_NUMBER`. Every other
entity type needs a backend that supports it, such as Rampart. The rule does
not inherit the client's `sensitiveInfoBackend` / `sensitive_info_backend`,
so listing `BANK_ACCOUNT` or `ROUTING_NUMBER` without passing `backend` to
the rule itself throws at construction. Share one instance:
```ts
const sensitiveInfoBackend = rampart();
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY!, sensitiveInfoBackend });
const detectPii = localDetectSensitiveInfo({
deny: ["BANK_ACCOUNT", "ROUTING_NUMBER"],
backend: sensitiveInfoBackend,
});
```
**Only some adapters map typed `inputs`.** A remote policy evaluates the
typed inputs a guard call submits. In JavaScript only
`@arcjet/guard/vercel-ai/v7` accepts `inputs` and `actor`; every other
JavaScript adapter takes `action` and SDK `rules` only, so a remote policy
has nothing to evaluate and none of its rules fire. Every Python adapter
accepts `inputs`. Use SDK `rules` where `inputs` is unavailable, and don't
assume a published policy is enforcing.
The builders differ by language. JavaScript uses one `policyInput` namespace
(`policyInput.server.string`, `policyInput.server.stringList`,
`policyInput.local.string`). Python uses two module-level objects,
`server_input` and `local_input` (`server_input.string`,
`server_input.string_list`, `local_input.string`). `SERVER` inputs go to
Arcjet; `LOCAL` inputs are evaluated on your machine, so the value never
leaves your application.
**The `inputs` and `actor` resolver arity varies by surface.** Python
`guard_tool` calls the resolver with the arguments mapping alone. CrewAI's
`register_arcjet_hooks` calls it with `(arguments, ctx)`, and LangChain's
`guard_tool` with `(arguments, config)`.
**Annotate the `rules` callback where `TInput` defaults to `unknown`.** On
the Genkit, OpenAI Agents, and Strands Agents JavaScript adapters,
`guardTool` cannot infer the tool input, so destructuring it is a type error.
Write `rules: (input: { body: string }) => [detectPii(input.body)]`.
**A missing decision is not a denial.** If the model asks a clarifying
question instead of calling the guarded tool, nothing is sent, no guard call
happens, and no decision is returned. That looks identical to a working
guard. When verifying an integration, read the decision in the Console or
your logs rather than concluding from the absence of a side effect. Give a
test agent a system prompt that tells it to complete the request without
follow-up questions, and to quote retrieved values verbatim: a model that
masks sensitive values itself leaves the rule nothing to detect, and the
guard then correctly allows.
**Guarding one tool only helps if it is the only path.** If the same session
also exposes an unguarded route to the capability, the model can take it. On
the Claude Agent SDK, `settingSources: []` / `setting_sources=[]` drops
CLAUDE.md and filesystem settings, and `strictMcpConfig: true` /
`strict_mcp_config=True` drops inherited MCP servers. Both are needed.
**Claude Agent SDK uses two distinct ids.** `ClaudeAgentOptions.sessionId` /
`session_id` names a new SDK session and must be unique per run; reusing one
fails with `Session ID ... is already in use`. The guard `sessionId` /
`session_id` identifies the actor and can be long-lived. An authored
`tool()` handler carries no session id of its own, so pass one to
`guardTool`.
**Claude Managed Agents correlates on your own id.**
`claudeManagedAgentsContext` / `claude_managed_agents_context` drops
Anthropic session and event ids (`sesn_…`, `sevt_…`), because they are not
ids you created. Pass a conversation id your application owns. Anthropic's
session id still addresses the session you send events to. In Python,
`guard_events` has no `inbound` option: `action` and `rules` sit at the top
level, it takes `send=`, and the callable it returns replaces `send` and
raises `ArcjetDeniedError` on a denial. Pass an async client, because a
blocking one makes the wrapper synchronous.
## Reference guides
### Features
- [Shield](https://docs.arcjet.com/shield)
- [Rate limiting](https://docs.arcjet.com/rate-limiting)
- [Bot protection](https://docs.arcjet.com/bot-protection)
- [Email validation](https://docs.arcjet.com/email-validation)
- [Sensitive information](https://docs.arcjet.com/sensitive-info)
- [Prompt injection](https://docs.arcjet.com/prompt-injection)
- [Content moderation](https://docs.arcjet.com/content-moderation)
- [Signup form protection](https://docs.arcjet.com/signup-protection)
- [Filters](https://docs.arcjet.com/filters)
- [AI protection](https://docs.arcjet.com/ai-protection)
- [Guards](https://docs.arcjet.com/guards)
- [Agent guard quick start](https://docs.arcjet.com/guards/quick-start)
- [Agent guard integrations](https://docs.arcjet.com/guards/framework-integrations)
- [Agent guard remote policies](https://docs.arcjet.com/guards/remote-policies)
- [Agent guard testing and reference](https://docs.arcjet.com/guards/reference)
- [Capture events](https://docs.arcjet.com/guards/capture)
- [Vercel AI SDK agent guard](https://docs.arcjet.com/guards/vercel-ai)
- [LangChain agent guard](https://docs.arcjet.com/guards/langchain)
- [CrewAI agent guard](https://docs.arcjet.com/guards/crewai)
- [LangGraph agent guard](https://docs.arcjet.com/guards/langgraph)
- [Genkit agent guard](https://docs.arcjet.com/guards/genkit)
- [Google ADK agent guard](https://docs.arcjet.com/guards/google-adk)
- [OpenAI Agents agent guard](https://docs.arcjet.com/guards/openai-agents)
- [Strands Agents agent guard](https://docs.arcjet.com/guards/strands-agents)
- [TanStack AI agent guard](https://docs.arcjet.com/guards/tanstack-ai)
- [Vercel Eve agent guard](https://docs.arcjet.com/guards/vercel-eve)
- [Mastra agent guard](https://docs.arcjet.com/guards/mastra)
- [Claude Agent SDK agent guard](https://docs.arcjet.com/guards/claude-agent-sdk)
- [Claude Managed Agents agent guard](https://docs.arcjet.com/guards/claude-managed-agents)
- [Nosecone security headers](https://docs.arcjet.com/nosecone/quick-start)
- [`@arcjet/redact`](https://docs.arcjet.com/redact/quick-start)
### SDKs
- [Astro](https://docs.arcjet.com/reference/astro)
- [Bun](https://docs.arcjet.com/reference/bun)
- [Deno](https://docs.arcjet.com/reference/deno)
- [Fastify](https://docs.arcjet.com/reference/fastify)
- [Go](https://docs.arcjet.com/reference/go)
- [NestJS](https://docs.arcjet.com/reference/nestjs)
- [Next.js](https://docs.arcjet.com/reference/nextjs)
- [Node.js](https://docs.arcjet.com/reference/nodejs)
- [Nuxt](https://docs.arcjet.com/reference/nuxt)
- [Python](https://docs.arcjet.com/reference/python)
- [React Router](https://docs.arcjet.com/reference/react-router)
- [Remix](https://docs.arcjet.com/reference/remix)
- [SvelteKit](https://docs.arcjet.com/reference/sveltekit)
## Support
See the troubleshooting guide at https://docs.arcjet.com/troubleshooting.
For help, email or [join the Discord
server](https://arcjet.com/discord).