{"id":50947333,"url":"https://github.com/arcjet/arcjet-go","last_synced_at":"2026-06-17T21:32:25.074Z","repository":{"id":358908302,"uuid":"1243633532","full_name":"arcjet/arcjet-go","owner":"arcjet","description":"Arcjet Go SDK. Stop bots and automated attacks from burning your AI budget, leaking data, or misusing tools with Arcjet's AI security building blocks.","archived":false,"fork":false,"pushed_at":"2026-06-08T17:28:58.000Z","size":2371,"stargazers_count":1,"open_issues_count":2,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-08T19:18:34.004Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/arcjet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-19T14:17:03.000Z","updated_at":"2026-06-02T19:43:01.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/arcjet/arcjet-go","commit_stats":null,"previous_names":["arcjet/arcjet-go"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/arcjet/arcjet-go","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcjet%2Farcjet-go","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcjet%2Farcjet-go/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcjet%2Farcjet-go/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcjet%2Farcjet-go/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/arcjet","download_url":"https://codeload.github.com/arcjet/arcjet-go/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcjet%2Farcjet-go/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34466928,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-17T02:00:05.408Z","response_time":127,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-17T21:32:20.613Z","updated_at":"2026-06-17T21:32:25.068Z","avatar_url":"https://github.com/arcjet.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ca href=\"https://arcjet.com\" target=\"_arcjet-home\"\u003e \u003cpicture\u003e \u003csource\n  media=\"(prefers-color-scheme: dark)\"\n    srcset=\"https://arcjet.com/logo/arcjet-dark-lockup-voyage-horizontal.svg\"\u003e\n\u003cimg src=\"https://arcjet.com/logo/arcjet-light-lockup-voyage-horizontal.svg\"\n  alt=\"Arcjet Logo\" height=\"128\" width=\"auto\"\u003e \u003c/picture\u003e \u003c/a\u003e\n\n# Arcjet - Go SDK\n\n\u003e [!IMPORTANT]\n\u003e The Go SDK is pre-release and unstable.\n\n[Arcjet](https://arcjet.com) is the runtime security platform that ships with your AI code. Stop bots and automated attacks from burning your AI budget, leaking data, or misusing tools with Arcjet's AI security building blocks.\n\nThis is the Go SDK for [Arcjet](https://arcjet.com) — use `arcjet.NewClient` for\n**request protection** in `net/http` handlers (and any router that exposes\n`*http.Request`) and `arcjet.NewGuardClient` for **guard protection** (AI agent\ntool calls, MCP servers, background jobs, queue workers).\n\n## Getting started\n\n### Install the Arcjet CLI\n\nThe CLI is used to log in, manage site keys, and install protection skills.\n\n**Homebrew (macOS and Linux):**\n\n```sh\nbrew install arcjet/tap/arcjet\n```\n\n**npx (Node.js)** — run any command without installing:\n\n```sh\nnpx @arcjet/cli \u003ccommand\u003e\n```\n\n**Or [download a binary](https://github.com/arcjet/arcjet-cli/releases)** for\nmacOS (Apple Silicon, Intel), Linux (x86_64, arm64), and Windows (x86_64,\narm64).\n\n\u003e Examples below use the `arcjet` binary. If you installed via npx, replace\n\u003e `arcjet` with `npx @arcjet/cli`.\n\n### Quick setup with an AI agent\n\n1. Log in with the CLI:\n   ```sh\n   arcjet auth login\n   ```\n2. Install the protection skill:\n   ```sh\n   npx skills add arcjet/skills\n   ```\n3. Tell your agent what to protect — it handles the rest.\n\n### Manual setup\n\n1. **Log in** with the CLI (or at [`app.arcjet.com`](https://app.arcjet.com)):\n   ```sh\n   arcjet auth login\n   ```\n2. `go get github.com/arcjet/arcjet-go`\n3. **Get your site key:**\n   ```sh\n   arcjet sites get-key\n   ```\n   Or copy it from the [Arcjet dashboard](https://app.arcjet.com).\n4. Set `ARCJET_KEY=ajkey_yourkey` in your environment.\n5. Protect a handler — see the [AI protection example](#quick-start) or\n   individual [feature examples](#features) below.\n\n### Get help\n\n[Join our Discord server](https://arcjet.com/discord) or [reach out for\nsupport](https://docs.arcjet.com/support).\n\n## Quick start\n\n\u003e **Note:** Create the client once at package scope and reuse it across\n\u003e handlers. For larger projects, move it into its own package\n\u003e (e.g. `internal/security/arcjet.go`) so handlers can import a single shared\n\u003e instance.\n\nProtect an AI chat endpoint with prompt injection detection, token budget\nrate limiting, and bot protection:\n\n```go\n// main.go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/arcjet/arcjet-go\"\n)\n\nvar arcjetKey = func() string {\n\tkey := os.Getenv(\"ARCJET_KEY\")\n\tif key == \"\" {\n\t\tlog.Fatal(\"ARCJET_KEY is required. Get one with: arcjet sites get-key\" +\n\t\t\t\" or from https://app.arcjet.com\")\n\t}\n\treturn key\n}()\n\n// Create a single Arcjet client and reuse it across requests.\nvar aj = must(arcjet.NewClient(arcjet.Config{\n\tKey: arcjetKey,\n\tRules: []arcjet.Rule{\n\t\t// Detect and block prompt injection attacks in user messages.\n\t\tarcjet.DetectPromptInjection(arcjet.PromptInjectionOptions{\n\t\t\tMode: arcjet.ModeLive,\n\t\t}),\n\t\t// Rate limit by token budget — refill 100 tokens every 60 seconds.\n\t\tarcjet.TokenBucket(arcjet.TokenBucketOptions{\n\t\t\tMode:            arcjet.ModeLive,\n\t\t\tCharacteristics: []string{\"userId\"},\n\t\t\tRefillRate:      100,\n\t\t\tInterval:        time.Minute,\n\t\t\tCapacity:        1000,\n\t\t}),\n\t\t// Block automated clients and scrapers from your AI endpoints.\n\t\tarcjet.DetectBot(arcjet.BotOptions{\n\t\t\tMode:  arcjet.ModeLive,\n\t\t\tAllow: []string{}, // empty = block all bots\n\t\t}),\n\t\t// Protect against common web attacks (SQLi, XSS, etc.).\n\t\tarcjet.Shield(arcjet.ShieldOptions{Mode: arcjet.ModeLive}),\n\t},\n}))\n\ntype chatRequest struct {\n\tMessage string `json:\"message\"`\n}\n\nfunc chat(w http.ResponseWriter, r *http.Request) {\n\tvar body chatRequest\n\tif err := json.NewDecoder(r.Body).Decode(\u0026body); err != nil {\n\t\thttp.Error(w, \"bad request\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tuserID := \"user_123\" // replace with real user ID from session\n\n\tdecision, err := aj.Protect(\n\t\tr.Context(),\n\t\tr,\n\t\tarcjet.WithRequested(5), // tokens consumed per request\n\t\tarcjet.WithCharacteristics(map[string]string{\"userId\": userID}),\n\t\tarcjet.WithDetectPromptInjectionMessage(body.Message), // scan for prompt injection\n\t)\n\tif err != nil {\n\t\t// Arcjet fails open — log and continue serving.\n\t\tlog.Printf(\"arcjet: %v\", err)\n\t} else if decision.IsDenied() {\n\t\tstatus := http.StatusForbidden\n\t\tif decision.Reason.IsRateLimit() {\n\t\t\tstatus = http.StatusTooManyRequests\n\t\t}\n\t\thttp.Error(w, \"denied\", status)\n\t\treturn\n\t}\n\n\t// Safe to pass body.Message to your LLM.\n\t_ = json.NewEncoder(w).Encode(map[string]string{\"reply\": \"...\"})\n}\n\nfunc main() {\n\thttp.HandleFunc(\"/chat\", chat)\n\tlog.Fatal(http.ListenAndServe(\":3000\", nil))\n}\n\nfunc must[T any](v T, err error) T {\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treturn v\n}\n```\n\nPass `r.Context()` to `Protect` (as the example does) so the call honors\nclient disconnects.\n\nCall `aj.Protect` inside each handler — once per request. Avoid wrapping it in\ngeneric `net/http` middleware that runs on every path (including static\nassets); you lose the ability to apply per-route rules and risk\ndouble-counting traffic.\n\n## Features\n\n| Feature | Request (`NewClient`) | Guard (`NewGuardClient`) |\n| --- | :---: | :---: |\n| Rate Limiting | ✅ | ✅ |\n| Prompt Injection Detection | ✅ | ✅ |\n| Sensitive Information Detection | ✅ | ✅ |\n| Bot Protection | ✅ | — |\n| Shield WAF | ✅ | — |\n| Email Validation | ✅ | — |\n| Request Filters | ✅ | — |\n| IP Analysis | ✅ | — |\n| Custom Rules | — | ✅ |\n\n- 🔒 [Prompt Injection Detection](#prompt-injection-detection) — detect and block\n  prompt injection attacks before they reach your LLM.\n- 🤖 [Bot Protection](#bot-protection) — stop scrapers, credential stuffers, and\n  AI crawlers from abusing your endpoints.\n- 🛑 [Rate Limiting](#rate-limiting) — token bucket, fixed window, and sliding\n  window algorithms; model AI token budgets per user.\n- 🛡️ [Shield WAF](#shield-waf) — protect against SQL injection, XSS, and other\n  common web attacks.\n- 📧 [Email Validation](#email-validation) — block disposable, invalid, and\n  undeliverable addresses at signup.\n- 🎯 [Request Filters](#request-filters) — expression-based rules on IP, path,\n  headers, and custom fields.\n- 🌐 [IP Analysis](#ip-analysis) — geolocation, ASN, VPN, proxy, Tor, and hosting\n  detection included with every request.\n- 🧩 [Arcjet Guard](#arcjet-guard) — lower-level API for AI agent tool calls and\n  background tasks where there is no HTTP request.\n\n### Which features do I need?\n\n| If your app has...      | Recommended features                                                                                                  |\n| ----------------------- | --------------------------------------------------------------------------------------------------------------------- |\n| LLM / AI chat endpoints | Prompt injection + token bucket rate limit + bot protection + shield                                                  |\n| AI agent tool calls     | [Arcjet Guard](#arcjet-guard) — rate limiting + prompt injection + custom rules                                       |\n| MCP servers             | [Arcjet Guard](#arcjet-guard) — tool calls run over stdio/SSE, not HTTP, so use guard rules at each tool call site    |\n| Background jobs/workers | [Arcjet Guard](#arcjet-guard) — no HTTP request at the protection site                                                |\n| Public API              | Rate limiting + bot protection + shield                                                                               |\n| Signup / login forms    | Email validation + bot protection + rate limiting (or [signup protection](https://docs.arcjet.com/signup-protection)) |\n| Internal / admin routes | Shield + request filters (country, VPN/proxy blocking)                                                                |\n| Any web application     | Shield + bot protection (good baseline for all apps)                                                                  |\n\nAll features can be combined in a single Arcjet client. Rules are evaluated\ntogether — if **any** rule denies the request, `decision.IsDenied()` returns\n`true`. Use `arcjet.ModeDryRun` on individual rules to test them before\nenforcing.\n\n## Installation\n\n```sh\ngo get github.com/arcjet/arcjet-go\n```\n\nThe SDK requires Go 1.25 or later.\n\n## Prompt injection detection\n\nDetect and block prompt injection attacks — attempts by users to hijack your\nLLM's behavior through crafted input — before they reach your model.\n\n```go\naj, err := arcjet.NewClient(arcjet.Config{\n\tKey: arcjetKey,\n\tRules: []arcjet.Rule{\n\t\tarcjet.DetectPromptInjection(arcjet.PromptInjectionOptions{\n\t\t\tMode: arcjet.ModeLive,\n\t\t}),\n\t},\n})\nif err != nil {\n\treturn err\n}\n\ndecision, err := aj.Protect(\n\tr.Context(),\n\tr,\n\tarcjet.WithDetectPromptInjectionMessage(body.Message),\n)\nif err != nil {\n\t// Fails open — log and continue.\n\treturn err\n}\n\nif decision.IsDenied() {\n\thttp.Error(w, \"Prompt injection detected\", http.StatusBadRequest)\n\treturn\n}\n\n// Safe to pass body.Message to your LLM.\n```\n\nSee the [Prompt Injection docs](https://docs.arcjet.com/prompt-injection) for\nmore details.\n\n## Bot protection\n\nManage traffic from automated clients. Block scrapers, credential stuffers, and\nAI crawlers, while allowing legitimate bots like search engines and monitors.\n\n```go\naj, err := arcjet.NewClient(arcjet.Config{\n\tKey: arcjetKey,\n\tRules: []arcjet.Rule{\n\t\tarcjet.DetectBot(arcjet.BotOptions{\n\t\t\tMode: arcjet.ModeLive,\n\t\t\tAllow: []string{\n\t\t\t\tarcjet.BotCategorySearchEngine, // Google, Bing, etc.\n\t\t\t\t// arcjet.BotCategoryMonitor,    // Uptime monitoring\n\t\t\t\t// arcjet.BotCategoryPreview,    // Link previews (Slack, Discord)\n\t\t\t\t// \"OPENAI_CRAWLER_SEARCH\",      // Allow a specific bot by name\n\t\t\t},\n\t\t}),\n\t},\n})\nif err != nil {\n\treturn err\n}\n\ndecision, err := aj.Protect(r.Context(), r)\nif err != nil {\n\treturn err\n}\n\nif decision.IsDenied() {\n\thttp.Error(w, \"Bot detected\", http.StatusForbidden)\n\treturn\n}\n\nif decision.IsSpoofedBot() {\n\thttp.Error(w, \"Spoofed bot\", http.StatusForbidden)\n\treturn\n}\n```\n\n### Bot categories\n\nConfigure rules using\n[categories](https://docs.arcjet.com/bot-protection/identifying-bots#bot-categories)\nor [specific bot identifiers](https://github.com/arcjet/well-known-bots):\n\n```go\narcjet.DetectBot(arcjet.BotOptions{\n\tMode: arcjet.ModeLive,\n\tAllow: []string{\n\t\tarcjet.BotCategorySearchEngine,\n\t\t\"OPENAI_CRAWLER_SEARCH\",\n\t},\n})\n```\n\nExported constants cover all built-in categories: `arcjet.BotCategoryAcademic`,\n`BotCategoryAdvertising`, `BotCategoryAI`, `BotCategoryAmazon`,\n`BotCategoryArchive`, `BotCategoryBotnet`, `BotCategoryFeedFetcher`,\n`BotCategoryGoogle`, `BotCategoryMeta`, `BotCategoryMicrosoft`,\n`BotCategoryMonitor`, `BotCategoryOptimizer`, `BotCategoryPreview`,\n`BotCategoryProgrammatic`, `BotCategorySearchEngine`, `BotCategorySlack`,\n`BotCategorySocial`, `BotCategoryTool`, `BotCategoryUnknown`,\n`BotCategoryVercel`, `BotCategoryYahoo`. Plain strings still work (e.g.\n`\"CATEGORY:AI\"` or `\"OPENAI_CRAWLER_SEARCH\"` for [specific bots by\nname](https://arcjet.com/bot-list)) — the constants exist for autocomplete and\nto catch typos at compile time.\n\nIf you specify an allow list, all other bots are denied. An empty allow list\nblocks all bots. The reverse applies for deny lists.\n\n### Verified vs. spoofed bots\n\nBots claiming to be well-known crawlers (e.g. Googlebot) are verified against\ntheir known IP ranges. Use `decision.IsSpoofedBot()` to check:\n\n```go\nif decision.IsSpoofedBot() {\n\thttp.Error(w, \"Spoofed bot\", http.StatusForbidden)\n\treturn\n}\n```\n\n`decision.IsVerifiedBot()` reports the opposite — a crawler whose IP matched\nits published ranges — which you may want to allow even when other signals\nwould deny. `decision.IsMissingUserAgent()` reports a request a bot rule denied\nfor having no `User-Agent` header, a common sign of an automated client.\n\nSee the [Bot Protection docs](https://docs.arcjet.com/bot-protection) for more\ndetails.\n\n## Rate limiting\n\nLimit request rates per IP, user, or any custom characteristic. Arcjet supports\ntoken bucket, fixed window, and sliding window algorithms. Token buckets are\nideal for controlling AI token budgets — set `Capacity` to the max tokens a\nuser can spend, `RefillRate` to how many tokens are restored per `Interval`,\nand deduct tokens per request via `arcjet.WithRequested(n)`. The `Interval`\naccepts a `time.Duration`. Use `Characteristics` to track limits per user\ninstead of per IP.\n\n### Token bucket (recommended for AI)\n\nRate limits track by IP address by default. To track per user, declare the key\nname in `Characteristics` on the rule, then pass the actual value via\n`arcjet.WithCharacteristics` at call time:\n\n```go\naj, err := arcjet.NewClient(arcjet.Config{\n\tKey: arcjetKey,\n\tRules: []arcjet.Rule{\n\t\tarcjet.TokenBucket(arcjet.TokenBucketOptions{\n\t\t\tMode:            arcjet.ModeLive,\n\t\t\tCharacteristics: []string{\"userId\"}, // or omit for IP-based\n\t\t\tRefillRate:      100,                // tokens added per interval\n\t\t\tInterval:        time.Minute,        // interval duration\n\t\t\tCapacity:        1000,               // maximum tokens per bucket\n\t\t}),\n\t},\n})\n\ndecision, err := aj.Protect(\n\tr.Context(),\n\tr,\n\tarcjet.WithRequested(5), // tokens consumed by this request\n\tarcjet.WithCharacteristics(map[string]string{\"userId\": \"user_123\"}),\n)\n\nif decision.IsDenied() {\n\thttp.Error(w, \"Rate limited\", http.StatusTooManyRequests)\n\treturn\n}\n```\n\nPut `Characteristics` on the specific rate-limit rule that needs it, not on the\nglobal client — that way different rules can key by different things.\n\n### Fixed window\n\n```go\narcjet.FixedWindow(arcjet.FixedWindowOptions{\n\tMode:        arcjet.ModeLive,\n\tWindow:      time.Minute,\n\tMaxRequests: 100,\n})\n```\n\n### Sliding window\n\n```go\narcjet.SlidingWindow(arcjet.SlidingWindowOptions{\n\tMode:        arcjet.ModeLive,\n\tInterval:    time.Minute,\n\tMaxRequests: 100,\n})\n```\n\nSee the [Rate Limiting docs](https://docs.arcjet.com/rate-limiting) for more\ndetails.\n\n### Rate limit response headers\n\nCall `arcjet.SetRateLimitHeaders(w, decision)` to advertise the limit to\nclients using the IETF [RateLimit header fields for HTTP][ratelimit-draft]\n(`RateLimit` and `RateLimit-Policy`). It is a no-op when the decision carries\nno rate limit, so you can call it unconditionally:\n\n```go\ndecision, _ := aj.Protect(r.Context(), r)\narcjet.SetRateLimitHeaders(w, decision)\nif decision.IsDenied() {\n\thttp.Error(w, \"Rate limited\", http.StatusTooManyRequests)\n\treturn\n}\n```\n\n[ratelimit-draft]: https://ietf-wg-httpapi.github.io/ratelimit-headers/draft-ietf-httpapi-ratelimit-headers.html\n\n## Protecting a signup form\n\n`arcjet.ProtectSignup` bundles the rules commonly used on a signup form — a\nsliding-window rate limit, bot detection, and email validation — into one\nslice you can hand to `Config.Rules`:\n\n```go\naj, err := arcjet.NewClient(arcjet.Config{\n\tKey: arcjetKey,\n\tRules: arcjet.ProtectSignup(arcjet.ProtectSignupOptions{\n\t\tRateLimit: arcjet.SlidingWindowOptions{Mode: arcjet.ModeLive, Interval: time.Hour, MaxRequests: 5},\n\t\tBots:      arcjet.BotOptions{Mode: arcjet.ModeLive, Allow: nil}, // block all bots\n\t\tEmail:     arcjet.EmailOptions{Mode: arcjet.ModeLive, Deny: []arcjet.EmailType{arcjet.EmailTypeDisposable, arcjet.EmailTypeInvalid, arcjet.EmailTypeNoMXRecords}},\n\t}),\n})\n```\n\nThe returned rules can be combined with others using `append`.\n\n## Sensitive information detection\n\nDetect and block personally identifiable information — emails, phone numbers,\nIP addresses, and credit card numbers — in text you pass to `Protect`.\nDetection runs **locally** via the bundled WebAssembly analyzer (the same\n`arcjet_analyze_js_req` component used by the JavaScript and Python SDKs), so\nthe scanned text never leaves the SDK. Pass the text to scan with\n`arcjet.WithSensitiveInfoValue`:\n\n```go\naj, err := arcjet.NewClient(arcjet.Config{\n\tKey: arcjetKey,\n\tRules: []arcjet.Rule{\n\t\tarcjet.SensitiveInfo(arcjet.SensitiveInfoOptions{\n\t\t\tMode: arcjet.ModeLive,\n\t\t\tDeny: []arcjet.EntityType{\n\t\t\t\tarcjet.SensitiveInfoEmail,\n\t\t\t\tarcjet.SensitiveInfoCreditCardNumber,\n\t\t\t},\n\t\t}),\n\t},\n})\n\ndecision, err := aj.Protect(\n\tr.Context(),\n\tr,\n\tarcjet.WithSensitiveInfoValue(\"User input to scan\"),\n)\nif decision.Reason.IsSensitiveInfo() {\n\thttp.Error(w, \"Sensitive information detected\", http.StatusBadRequest)\n\treturn\n}\n```\n\n`Allow` and `Deny` are mutually exclusive: `Deny` blocks the listed entity\ntypes, while `Allow` blocks every type _except_ those listed. The built-in\nentity types are `SensitiveInfoEmail`, `SensitiveInfoPhoneNumber`,\n`SensitiveInfoIPAddress`, and `SensitiveInfoCreditCardNumber`.\n\nTo detect custom entities, set `Config.SensitiveInfoDetect`. It receives the\ntokenized text and returns one `EntityType` per token (empty leaves a token\nunclassified); the returned label can be any custom string you then list in\n`Allow`/`Deny`:\n\n```go\naj, err := arcjet.NewClient(arcjet.Config{\n\tKey: arcjetKey,\n\tSensitiveInfoDetect: func(ctx context.Context, tokens []string) []arcjet.EntityType {\n\t\tout := make([]arcjet.EntityType, len(tokens))\n\t\tfor i, tok := range tokens {\n\t\t\tif strings.HasPrefix(tok, \"sk-\") {\n\t\t\t\tout[i] = \"API_KEY\"\n\t\t\t}\n\t\t}\n\t\treturn out\n\t},\n\tRules: []arcjet.Rule{\n\t\tarcjet.SensitiveInfo(arcjet.SensitiveInfoOptions{\n\t\t\tMode: arcjet.ModeLive,\n\t\t\tDeny: []arcjet.EntityType{\"API_KEY\"},\n\t\t}),\n\t},\n})\n```\n\nTo redact rather than block sensitive information, see\n[Redacting sensitive information](#redacting-sensitive-information) below.\n\nSee the [Sensitive Information docs](https://docs.arcjet.com/sensitive-info)\nfor more details.\n\n## Redacting sensitive information\n\nThe `github.com/arcjet/arcjet-go/redact` package detects and redacts sensitive\ninformation — emails, phone numbers, IP addresses, credit card numbers, and\ncustom entities — entirely in-process. The text is never sent to Arcjet, which\nmakes it suitable for scrubbing prompts before they reach a third-party LLM and\nrestoring the values in the response. It runs the same WebAssembly component as\n`@arcjet/redact` and `arcjet.redact`, so all three SDKs redact identically.\n\n```go\nimport \"github.com/arcjet/arcjet-go/redact\"\n\nr, err := redact.New(ctx, redact.Options{})\nif err != nil {\n\tlog.Fatal(err)\n}\ndefer r.Close(ctx)\n\nredacted, unredact, err := r.Redact(ctx, \"Contact me at test@example.com\")\n// redacted == \"Contact me at \u003cRedacted email #0\u003e\"\n\n// ...send `redacted` to the model, then restore the originals in the reply:\nanswer := unredact(modelResponse)\n```\n\n`redact.New` compiles the component once; reuse the `Redactor` across calls.\nUse `Options.Entities` to limit which entity types are redacted, and\n`Options.Detect` / `Options.Replace` to plug in custom detection and\nreplacement logic. See the [Redaction docs](https://docs.arcjet.com/redact) for\nmore details.\n\n## Shield WAF\n\nProtect against common web attacks including SQL injection, XSS, path\ntraversal, and other OWASP Top 10 threats. No additional configuration\nneeded — Shield analyzes request patterns automatically.\n\n```go\narcjet.Shield(arcjet.ShieldOptions{Mode: arcjet.ModeLive})\n```\n\nAlways include `Shield` on the shared client as a base rule — it costs nothing\nto add and protects every route.\n\nSee the [Shield docs](https://docs.arcjet.com/shield) for more details.\n\n## Email validation\n\nPrevent users from signing up with disposable, invalid, or undeliverable email\naddresses. Deny types: `DISPOSABLE`, `FREE`, `INVALID`, `NO_MX_RECORDS`,\n`NO_GRAVATAR`.\n\n```go\naj, err := arcjet.NewClient(arcjet.Config{\n\tKey: arcjetKey,\n\tRules: []arcjet.Rule{\n\t\tarcjet.ValidateEmail(arcjet.EmailOptions{\n\t\t\tMode: arcjet.ModeLive,\n\t\t\tDeny: []arcjet.EmailType{\n\t\t\t\tarcjet.EmailTypeDisposable,\n\t\t\t\tarcjet.EmailTypeInvalid,\n\t\t\t\tarcjet.EmailTypeNoMXRecords,\n\t\t\t},\n\t\t}),\n\t},\n})\n\n// Pass the email with each Protect call.\ndecision, err := aj.Protect(\n\tr.Context(),\n\tr,\n\tarcjet.WithEmail(\"user@example.com\"),\n)\n```\n\nSee the [Email Validation docs](https://docs.arcjet.com/email-validation) for\nmore details.\n\n## Request filters\n\nFilter requests using expression-based rules against request properties (IP\naddress, headers, path, HTTP method, and custom local fields).\n\n### Block by country\n\nRestrict access to specific countries — useful for licensing, compliance, or\nregional rollouts. The `Allow` list denies all countries not listed:\n\n```go\naj, err := arcjet.NewClient(arcjet.Config{\n\tKey: arcjetKey,\n\tRules: []arcjet.Rule{\n\t\t// Allow only US traffic — all other countries are denied.\n\t\tarcjet.Filter(arcjet.FilterOptions{\n\t\t\tMode:  arcjet.ModeLive,\n\t\t\tAllow: []string{`ip.src.country == \"US\"`},\n\t\t}),\n\t},\n})\n\ndecision, err := aj.Protect(r.Context(), r)\nif decision.IsDenied() {\n\thttp.Error(w, \"Access restricted in your region\", http.StatusForbidden)\n\treturn\n}\n```\n\nTo restrict to a specific state or province, combine country and region:\n\n```go\narcjet.Filter(arcjet.FilterOptions{\n\tMode: arcjet.ModeLive,\n\t// Allow only California — useful for state-level compliance e.g. CCPA testing.\n\tAllow: []string{`ip.src.country == \"US\" \u0026\u0026 ip.src.region == \"California\"`},\n})\n```\n\n### Block VPN and proxy traffic\n\nPrevent anonymized traffic from accessing sensitive endpoints — useful for\nfraud prevention, enforcing geo-restrictions, and reducing abuse:\n\n```go\narcjet.Filter(arcjet.FilterOptions{\n\tMode: arcjet.ModeLive,\n\tDeny: []string{\n\t\t\"ip.src.vpn\",   // VPN services\n\t\t\"ip.src.proxy\", // Open proxies\n\t\t\"ip.src.tor\",   // Tor exit nodes\n\t},\n})\n```\n\nFor cases where you want to allow some anonymized traffic (e.g. Apple Private\nRelay) but still log or handle it differently, use `decision.IP` helpers after\ncalling `Protect`:\n\n```go\ndecision, err := aj.Protect(r.Context(), r)\nif err != nil {\n\treturn err\n}\n\nif decision.IP.IsVPN || decision.IP.IsTor {\n\thttp.Error(w, \"VPN traffic not allowed\", http.StatusForbidden)\n\treturn\n}\nif decision.IP.IsRelay {\n\t// Privacy relay (e.g. Apple Private Relay) — lower risk than a VPN.\n\t// Allow through with custom handling.\n}\n```\n\n### Custom local fields\n\nPass arbitrary values from your application for use in filter expressions:\n\n```go\ndecision, err := aj.Protect(\n\tr.Context(),\n\tr,\n\tarcjet.WithFilterLocal(map[string]string{\n\t\t\"userId\": currentUser.ID,\n\t\t\"plan\":   currentUser.Plan,\n\t}),\n)\n```\n\nThese are then available as `local[\"userId\"]` and `local[\"plan\"]` in\nexpressions:\n\n```go\narcjet.Filter(arcjet.FilterOptions{\n\tMode: arcjet.ModeLive,\n\tDeny: []string{`local[\"plan\"] == \"free\" \u0026\u0026 ip.src.country != \"US\"`},\n})\n```\n\n`WithFilterLocal` values stay local — they are evaluated by the embedded\nWebAssembly runtime and are never sent to Arcjet Cloud.\n\nSee the [Request Filters docs](https://docs.arcjet.com/filters),\n[IP Geolocation blueprint](https://docs.arcjet.com/blueprints/ip-geolocation),\nand [VPN/Proxy Detection\nblueprint](https://docs.arcjet.com/blueprints/vpn-proxy-detection) for more\ndetails.\n\n## IP analysis\n\nArcjet returns IP metadata with every decision — no extra API calls needed.\n\n```go\nif decision.IP.IsHosting {\n\t// Likely a cloud/hosting provider — often suspicious for bots.\n\thttp.Error(w, \"Hosting IP blocked\", http.StatusForbidden)\n\treturn\n}\n\nif decision.IP.IsVPN || decision.IP.IsProxy || decision.IP.IsTor {\n\t// Apply your policy for anonymized traffic.\n}\n\nip := decision.IP\nlog.Println(ip.City, ip.CountryName) // geolocation\nlog.Println(ip.ASN, ip.ASNName)      // ASN / network\nlog.Println(ip.IsVPN, ip.IsHosting)  // reputation\n```\n\nAvailable fields include geolocation (`Latitude`, `Longitude`, `City`,\n`Region`, `Country`, `Continent`), network (`ASN`, `ASNName`, `ASNDomain`,\n`ASNType`, `ASNCountry`), and reputation (`IsVPN`, `IsProxy`, `IsTor`,\n`IsHosting`, `IsRelay`).\n\n## Arcjet Guard\n\n`arcjet.NewGuardClient` is a lower-level API designed for AI agent tool calls\nand background tasks where there is no HTTP request object. It gives you\nfine-grained, per-call control over rate limiting, prompt injection detection,\nsensitive information detection (via `GuardSensitiveInfo`), and custom rules.\n\n### How it differs from `NewClient`\n\n| | `NewClient` (request protection) | `NewGuardClient` (guard) |\n| --- | --- | --- |\n| **Designed for** | HTTP request protection | AI agent tool calls, background jobs |\n| **Request object** | Required (`Protect(ctx, r, ...)`) | Not needed |\n| **Rule binding** | Rules configured once, input via `Protect` options | Rules configured once, bound with input per invocation |\n| **Rate limit key** | IP or `WithCharacteristics` | Explicit key string (SHA-256 hashed before sending) |\n| **Custom rules** | Not supported | `GuardCustom` |\n\n### Installation\n\nGuard is part of the same `github.com/arcjet/arcjet-go` module — no extra\ninstall required.\n\n### Quick start\n\nDeclare the guard client and each rule once at package scope. Call `guard.Guard`\nat each specific operation with a **hardcoded** `Label` so the dashboard groups\ncalls by what they actually are.\n\n```go\npackage agent\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/arcjet/arcjet-go\"\n)\n\n// Single guard client — reused across calls.\nvar guard = must(arcjet.NewGuardClient(arcjet.GuardConfig{\n\tKey: os.Getenv(\"ARCJET_KEY\"),\n}))\n\n// Rules configured once. Each rule's Bucket groups its counters server-side.\nvar (\n\tuserLimit = must(arcjet.GuardTokenBucket(arcjet.GuardTokenBucketOptions{\n\t\tMode:       arcjet.ModeLive,\n\t\tRefillRate: 100,\n\t\tInterval:   time.Minute,\n\t\tCapacity:   1000,\n\t\tBucket:     \"agent.user-tokens\", // distinct from any Label\n\t}))\n\tpromptScan = must(arcjet.GuardPromptInjection(arcjet.GuardPromptInjectionOptions{Mode: arcjet.ModeLive}))\n)\n\n// GetWeather is an agent tool. Guard at the tool function (or at the\n// dispatch arm right before calling it) — never in a generic\n// handleToolCall(name, args) wrapper with an interpolated label.\nfunc GetWeather(ctx context.Context, userID, message string) error {\n\tdecision, err := guard.Guard(ctx, arcjet.GuardRequest{\n\t\tLabel: \"tools.get_weather\", // hardcoded string, not fmt.Sprintf\n\t\tMetadata: map[string]string{\n\t\t\t\"userId\": userID,\n\t\t},\n\t\tRules: []arcjet.GuardRuleInput{\n\t\t\tuserLimit.Key(userID, 1),\n\t\t\tpromptScan.Text(message),\n\t\t},\n\t})\n\tif err != nil {\n\t\t// Guard fails open — log and continue.\n\t\treturn nil\n\t}\n\tif decision.IsDenied() {\n\t\t// Use the per-rule accessor to recover details — rate limit reset\n\t\t// time, prompt injection signal — so the caller knows WHY.\n\t\tif r := userLimit.DeniedResult(decision); r != nil {\n\t\t\treturn fmt.Errorf(\"rate limited — resets at unix %d\", r.ResetAtUnixSeconds)\n\t\t}\n\t\tif promptScan.DeniedResult(decision) != nil {\n\t\t\treturn errors.New(\"input flagged as prompt injection\")\n\t\t}\n\t\treturn errors.New(\"blocked\")\n\t}\n\n\t// ... do the work.\n\treturn nil\n}\n```\n\n\u003e **Note:** The `Label` argument should be a hardcoded string like\n\u003e `\"tools.get_weather\"`, not `fmt.Sprintf(\"tools.%s\", name)`. Hardcoded labels\n\u003e stay greppable, and the dashboard groups by them. Interpolation produces a\n\u003e sea of distinct-looking entries instead of one bucket per operation.\n\n### Rate limiting\n\nToken bucket, fixed window, and sliding window algorithms are available.\nConfigure the rule once, then call `.Key(key, requested)` with a key and\noptional requested count for each invocation.\n\nEvery guard rate-limit rule requires an explicit `Key` at call time. There is\nno IP fallback — guard runs where there is no HTTP context. When there is no\nper-user context (e.g. a stdio MCP server or a single-tenant worker), pick a\nstable identifier such as the deployment name or `\"default\"` and add a comment\nexplaining why.\n\n#### Token bucket\n\n```go\nuserLimit, err := arcjet.GuardTokenBucket(arcjet.GuardTokenBucketOptions{\n\tMode:       arcjet.ModeLive,\n\tRefillRate: 100,         // tokens added per interval\n\tInterval:   time.Minute, // refill interval\n\tCapacity:   1000,        // maximum bucket capacity\n\tBucket:     \"agent.user-tokens\",\n})\n\n// At call time:\ndecision, err := guard.Guard(ctx, arcjet.GuardRequest{\n\tLabel: \"tools.get_weather\",\n\tRules: []arcjet.GuardRuleInput{userLimit.Key(userID, 5)},\n})\n```\n\n#### Fixed window\n\n```go\nteamLimit, err := arcjet.GuardFixedWindow(arcjet.GuardFixedWindowOptions{\n\tMode:        arcjet.ModeLive,\n\tWindow:      time.Hour,\n\tMaxRequests: 1000,\n\tBucket:      \"api.search\",\n})\n\ndecision, err := guard.Guard(ctx, arcjet.GuardRequest{\n\tLabel: \"api.search\",\n\tRules: []arcjet.GuardRuleInput{teamLimit.Key(teamID, 1)},\n})\n```\n\n#### Sliding window\n\n```go\napiLimit, err := arcjet.GuardSlidingWindow(arcjet.GuardSlidingWindowOptions{\n\tMode:        arcjet.ModeLive,\n\tInterval:    time.Minute,\n\tMaxRequests: 500,\n\tBucket:      \"api.query\",\n})\n\ndecision, err := guard.Guard(ctx, arcjet.GuardRequest{\n\tLabel: \"api.query\",\n\tRules: []arcjet.GuardRuleInput{apiLimit.Key(userID, 1)},\n})\n```\n\n### Prompt injection detection\n\nUse on any untrusted text before it reaches a model or tool argument — and on\ntool call *results* when the tool fetches content from untrusted sources.\n\n```go\npromptScan, err := arcjet.GuardPromptInjection(arcjet.GuardPromptInjectionOptions{Mode: arcjet.ModeLive})\n\ndecision, err := guard.Guard(ctx, arcjet.GuardRequest{\n\tLabel: \"tools.get_weather\",\n\tRules: []arcjet.GuardRuleInput{promptScan.Text(userMessage)},\n})\n\nif decision.IsDenied() \u0026\u0026 decision.Reason == arcjet.ReasonPromptInjection {\n\treturn errors.New(\"prompt injection detected\")\n}\n```\n\n### Custom rules\n\nDefine custom local rules with `GuardCustom`. The evaluation function runs in\nyour process — Arcjet never executes it. The `Input` map you pass at call time\nis submitted to Arcjet alongside the function's `Conclusion` and any opaque\n`Data`, so the dashboard can show what the rule saw. Do not pass raw secrets\nor unhashed PII through `Input`; hash or redact first if the inputs are\nsensitive.\n\n```go\ntopicRule, err := arcjet.GuardCustom(arcjet.GuardCustomOptions{\n\tMode:   arcjet.ModeLive,\n\tConfig: map[string]string{\"blocked_topic\": \"weapons\"},\n\tFunc: func(ctx context.Context, input map[string]string) (arcjet.GuardCustomResult, error) {\n\t\tif input[\"topic\"] == \"weapons\" {\n\t\t\treturn arcjet.GuardCustomResult{\n\t\t\t\tConclusion: arcjet.ConclusionDeny,\n\t\t\t\tData:       map[string]string{\"matched\": input[\"topic\"]},\n\t\t\t}, nil\n\t\t}\n\t\treturn arcjet.GuardCustomResult{Conclusion: arcjet.ConclusionAllow}, nil\n\t},\n})\n\ndecision, err := guard.Guard(ctx, arcjet.GuardRequest{\n\tLabel: \"content\",\n\tRules: []arcjet.GuardRuleInput{\n\t\ttopicRule.Input(map[string]string{\"topic\": userTopic}),\n\t},\n})\n```\n\n### Per-rule results\n\n`decision.Results` contains one entry per rule invocation, with typed result\ndetails for inspection:\n\n```go\nfor _, result := range decision.Results {\n\tswitch {\n\tcase result.TokenBucket != nil:\n\t\tlog.Printf(\"rate limit: %d / %d remaining; resets at %d\",\n\t\t\tresult.TokenBucket.RemainingTokens,\n\t\t\tresult.TokenBucket.MaxTokens,\n\t\t\tresult.TokenBucket.ResetAtUnixSeconds)\n\tcase result.PromptInjection != nil:\n\t\tlog.Printf(\"prompt injection detected=%v\", result.PromptInjection.Detected)\n\tcase result.LocalSensitiveInfo != nil:\n\t\tlog.Printf(\"sensitive info detected=%v types=%v\",\n\t\t\tresult.LocalSensitiveInfo.Detected,\n\t\t\tresult.LocalSensitiveInfo.DetectedEntityTypes)\n\t}\n\tif result.IsDenied() {\n\t\tlog.Printf(\"denied by %s\", result.Reason)\n\t}\n}\n```\n\n### Decision API\n\n```go\ndecision, err := guard.Guard(ctx, arcjet.GuardRequest{\n\tLabel: \"tools.get_weather\",\n\tRules: []arcjet.GuardRuleInput{userLimit.Key(userID, 5)},\n})\n\n// Layer 1: conclusion and reason.\ndecision.Conclusion // arcjet.ConclusionAllow or arcjet.ConclusionDeny\ndecision.Reason     // arcjet.ReasonRateLimit, ReasonPromptInjection, etc.\n\n// Layer 2: error detection.\ndecision.IsErrored() // true if any rule errored or the server reported diagnostics\n\n// Layer 3: per-rule results (see \"Per-rule results\" above).\nfor _, result := range decision.Results {\n\tlog.Println(result.Type, result.Conclusion)\n}\n```\n\n### `Guard` parameter reference\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `Rules` | `[]arcjet.GuardRuleInput` | Bound rule inputs (required) |\n| `Label` | `string` | Hardcoded label identifying this guard call (required) |\n| `Metadata` | `map[string]string` | Optional key-value metadata recorded in the dashboard |\n\n### DRY_RUN mode\n\nAll guard rules accept a `Mode` parameter. Use `arcjet.ModeDryRun` to evaluate\nrules without blocking:\n\n```go\nuserLimit, err := arcjet.GuardTokenBucket(arcjet.GuardTokenBucketOptions{\n\tMode:       arcjet.ModeDryRun,\n\tRefillRate: 10,\n\tInterval:   time.Minute,\n\tCapacity:   100,\n\tBucket:     \"agent.user-tokens\",\n})\n```\n\n## Best practices\n\n### Single-instance pattern\n\nCreate one Arcjet client at startup and reuse it across all handlers:\n\n```go\n// Good — one instance, created once at package scope.\nvar aj = must(arcjet.NewClient(arcjet.Config{Key: arcjetKey, Rules: []arcjet.Rule{...}}))\n\n// Bad — new client per request wastes resources and creates a new HTTP/2\n// connection each time.\nfunc handler(w http.ResponseWriter, r *http.Request) {\n\taj, _ := arcjet.NewClient(arcjet.Config{...}) // don't do this\n}\n```\n\n### Shared client in its own package\n\nFor larger projects, put the client in its own package (e.g.\n`internal/security/arcjet.go`) and import it from handlers. Always include\n`Shield` as a base rule, then layer route-specific rules with `WithRule`:\n\n```go\n// internal/security/arcjet.go\npackage security\n\nimport (\n\t\"os\"\n\n\t\"github.com/arcjet/arcjet-go\"\n)\n\nvar Client = must(arcjet.NewClient(arcjet.Config{\n\tKey: os.Getenv(\"ARCJET_KEY\"),\n\tRules: []arcjet.Rule{\n\t\tarcjet.Shield(arcjet.ShieldOptions{Mode: arcjet.ModeLive}),\n\t\tarcjet.DetectBot(arcjet.BotOptions{Mode: arcjet.ModeLive, Allow: []string{}}),\n\t},\n}))\n```\n\n```go\n// internal/http/chat.go\npackage http\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/arcjet/arcjet-go\"\n\t\"example.com/app/internal/security\"\n)\n\n// Layer per-route rules without mutating the shared client. WithRule\n// validates and pre-builds the rule's wire form, so it returns an error\n// for misconfigured rules — keep the call at package scope so a bad rule\n// fails at startup instead of on the first request.\nvar chatClient = must(security.Client.WithRule(arcjet.TokenBucket(arcjet.TokenBucketOptions{\n\tMode:            arcjet.ModeLive,\n\tCharacteristics: []string{\"userId\"},\n\tRefillRate:      100,\n\tInterval:        time.Minute,\n\tCapacity:        1000,\n})))\n\nfunc must[T any](v T, err error) T {\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treturn v\n}\n```\n\n`WithRule` returns a copy — the original client is left unchanged, so the same\nbase instance can be specialised for many routes.\n\n### DRY_RUN mode for testing\n\nUse `arcjet.ModeDryRun` to test rules without blocking traffic. Decisions are\nlogged but requests are allowed through:\n\n```go\narcjet.DetectBot(arcjet.BotOptions{Mode: arcjet.ModeDryRun, Allow: []string{}})\narcjet.TokenBucket(arcjet.TokenBucketOptions{\n\tMode:       arcjet.ModeDryRun,\n\tRefillRate: 5,\n\tInterval:   10 * time.Second,\n\tCapacity:   10,\n})\n```\n\n### Proxy configuration\n\nWhen running behind a load balancer or reverse proxy, configure trusted IPs so\nArcjet resolves the real client IP from `X-Forwarded-For`:\n\n```go\naj, err := arcjet.NewClient(arcjet.Config{\n\tKey:     arcjetKey,\n\tRules:   []arcjet.Rule{...},\n\tProxies: []string{\"10.0.0.0/8\", \"192.168.0.1\"},\n})\n```\n\n### Hosting platform\n\nWhen deployed on a managed hosting platform, Arcjet reads the client IP from\nthat platform's signed headers. Fly.io, Vercel, Render, Firebase, Railway, and\nCloudflare Pages are auto-detected from their environment variables. If your\nservice runs behind a platform that isn't auto-detected — most importantly a Go\norigin behind the **Cloudflare CDN**, which doesn't set `CF_PAGES` — set\n`Config.Platform` explicitly so Arcjet trusts the platform header (e.g.\n`CF-Connecting-IP`) instead of guessing:\n\n```go\naj, err := arcjet.NewClient(arcjet.Config{\n\tKey:      arcjetKey,\n\tRules:    []arcjet.Rule{...},\n\tPlatform: arcjet.PlatformCloudflare,\n})\n```\n\n`NewClient` returns `ErrInvalidPlatform` for an unrecognized value.\n\n### `Protect` parameter reference\n\nAll options are optional and passed alongside the `*http.Request`:\n\n| Option                                     | Used by                                            |\n| ------------------------------------------ | -------------------------------------------------- |\n| `WithRequested(int)`                       | Token bucket rate limit                            |\n| `WithCharacteristic(key, value string)`    | Rate limiting — single key/value                   |\n| `WithCharacteristics(map[string]string)`   | Rate limiting (values for keys declared in rules)  |\n| `WithDetectPromptInjectionMessage(string)` | Prompt injection detection                         |\n| `WithSensitiveInfoValue(string)`           | Sensitive information detection (text scanned locally)            |\n| `WithEmail(string)`                        | Email validation                                   |\n| `WithFilterLocal(map[string]string)`       | Request filters using `local[\"field\"]` expressions |\n| `WithIPSrc(string)`                        | Manual IP override (advanced)                      |\n| `WithBody([]byte)`                         | Request body override                              |\n| `WithExtra(map[string]string)`             | Additional fields sent to Arcjet                   |\n\n### Decision response\n\n```go\ndecision, err := aj.Protect(r.Context(), r)\n\n// Top-level checks.\ndecision.IsDenied()    // true if any LIVE rule denied the request\ndecision.IsAllowed()   // true if all rules allowed the request\ndecision.IsErrored()   // true if Arcjet encountered an error (fails open)\n\n// Branch on reason for actionable error responses. Only branch on reasons that\n// produce a different response — a SHIELD arm returning 403 when the default\n// already returns 403 is dead code.\nswitch {\ncase decision.Reason.IsRateLimit():\n\tlog.Println(decision.Reason.RateLimit.Remaining)\ncase decision.Reason.IsBot():\n\tlog.Println(decision.Reason.Bot.Denied)\n}\n\n// Per-rule results.\nfor _, result := range decision.Results {\n\tlog.Println(result.Reason.Type, result.Conclusion)\n}\n```\n\n### Error handling\n\nArcjet is designed to fail open — if the service is unavailable, requests are\nallowed through. Check for errors explicitly if your use case requires it:\n\n```go\ndecision, err := aj.Protect(r.Context(), r)\nif err != nil {\n\t// Arcjet service error — fail open or apply fallback policy.\n\tlog.Printf(\"arcjet: %v\", err)\n} else if decision.IsDenied() {\n\thttp.Error(w, \"Denied\", http.StatusForbidden)\n\treturn\n}\n```\n\nConfiguration and validation errors wrap exported sentinels so they can be\ndetected with `errors.Is`:\n\n```go\naj, err := arcjet.NewClient(arcjet.Config{Key: \"\"})\nif errors.Is(err, arcjet.ErrMissingKey) {\n\tlog.Fatal(\"set ARCJET_KEY in your environment\")\n}\n```\n\nAvailable sentinels: `ErrMissingKey`, `ErrNilClient`, `ErrNilRequest`,\n`ErrNilRule`, `ErrInvalidMode`, `ErrAllowDenyConflict`, `ErrInvalidProxy`,\n`ErrInvalidLabel`, `ErrInvalidRateLimit`, `ErrEmptyKey`, `ErrMissingFunc`,\n`ErrInvalidWasm`, `ErrWasmClosed`, `ErrWasmExportNotFound`, `ErrEmptyResponse`.\n\nRemote and per-rule errors are surfaced as `ArcjetError` values with a `Code`\nfield. Match a specific server error code with `errors.Is`:\n\n```go\nif errors.Is(err, arcjet.ArcjetError{Code: \"AJ1100\"}) {\n\t// handle a specific Arcjet error code\n}\n```\n\n`Decision.Err()` and `GuardDecision.Err()` return the underlying `ArcjetError`\n(or `nil`) when the decision errored — useful for bubbling Arcjet errors out\nof helpers.\n\n## Verify decisions\n\nAfter wiring up protection, confirm it is actually firing. There is no shortcut\nto seeing a real decision — trigger one, then check the platform.\n\n1. Build and start the app (`go build ./... \u0026\u0026 ./your-app`).\n2. Trigger a real request, e.g. `curl http://localhost:3000/chat`. To trip a\n   rate limit, loop the call:\n   ```sh\n   for i in {1..50}; do curl -s -o /dev/null -w \"%{http_code}\\n\" http://localhost:3000/chat; done\n   ```\n   For guard calls, invoke the protected function directly (`go run ./cmd/agent-smoketest`)\n   rather than trying to `curl` something — there is no HTTP surface.\n3. Confirm the decision via the Arcjet CLI:\n   ```sh\n   arcjet requests list --site-id \u003cid\u003e   # request protection\n   arcjet guards list --site-id \u003cid\u003e     # guard\n   arcjet requests explain --site-id \u003cid\u003e --request-id \u003cid\u003e\n   ```\n\nThe dashboard at [app.arcjet.com](https://app.arcjet.com) shows the same data\nwith filtering and history.\n\n## Gotchas\n\n- **Wrong client**: `NewClient` is for HTTP routes; `NewGuardClient` is for\n  non-HTTP code (tool calls, MCP servers, queue workers, background jobs).\n  Using the wrong one is the most common mistake — MCP \"servers\" don't receive\n  HTTP requests, so they use `NewGuardClient`.\n- **Wrong placement**: `Protect` belongs inside each handler, not in generic\n  middleware that wraps every request including static assets.\n- **Wrong layer for `Guard`**: don't put `guard.Guard` in a\n  `handleToolCall(name, args)` dispatcher. Put it inside each specific tool\n  function (or the dispatch arm right before the call) so the `Label` and\n  `Metadata` can be hardcoded.\n- **Interpolated labels**: `Label: fmt.Sprintf(\"tools.%s\", name)` defeats the\n  dashboard grouping. Use a hardcoded string per tool.\n- **Double-counting**: calling `Protect` or `Guard` multiple times for the same\n  operation counts against rate limits multiple times.\n- **Hardcoded keys**: never hardcode `ARCJET_KEY` — read it from the\n  environment. Don't commit it to source.\n\n## Support\n\nThis repository follows the [Arcjet Support\nPolicy](https://docs.arcjet.com/support).\n\n## Security\n\nThis repository follows the [Arcjet Security\nPolicy](https://docs.arcjet.com/security).\n\n## License\n\nLicensed under the [Apache License, Version\n2.0](http://www.apache.org/licenses/LICENSE-2.0).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farcjet%2Farcjet-go","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farcjet%2Farcjet-go","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farcjet%2Farcjet-go/lists"}