Skip to content

Sensitive information reference for Go

Arcjet Sensitive Information Detection protects against clients sending you sensitive information such as PII that you do not wish to handle. All detection runs locally in your own environment through a pluggable detection backend: a built-in WebAssembly engine (the default) or the on-device Rampart NER model, which detects names, addresses, and government or financial identifiers in addition to the structured types.

Sensitive information detection is configured by allowing or denying a subset of sensitive information. The allow and deny lists are mutually exclusive. With allow, Arcjet denies any detected sensitive information that the list does not name. With deny, Arcjet allows any detected sensitive information that the list does not name.

The arcjet client can be configured with one or more Sensitive information rules. Construct them with sensitiveInfo(...) in JavaScript / TypeScript, detect_sensitive_info(...) in Python, or arcjet.SensitiveInfo(arcjet.SensitiveInfoOptions{...}) in Go. The lists are allow / deny in JS and Python, and Allow / Deny in Go.

The optional backend selects which detection backend runs. Omit it to use the built-in engine. Pass rampart() (JS / Python) or Backend from rampart.New (Go) for on-device NER.

type SensitiveInfoOptionsAllow = {
mode?: "LIVE" | "DRY_RUN";
allow?: Array<ArcjetSensitiveInfoType>;
contextWindowSize?: number;
// You can also provide a custom detection function to detect other types
// of sensitive information (see below).
detect?: (tokens: string[]) -> Array<SensitiveInfoType | undefined>;
// Optionally swap the default WebAssembly detection engine for a custom
// backend, such as the on-device Rampart NER model (see below).
backend?: SensitiveInfoBackend;
};
type SensitiveInfoOptionsDeny = {
mode?: "LIVE" | "DRY_RUN";
deny?: Array<ArcjetSensitiveInfoType>;
contextWindowSize?: number;
// You can also provide a custom detection function to detect other types
// of sensitive information (see below).
detect?: (tokens: string[]) -> Array<SensitiveInfoType | undefined>;
// Optionally swap the default WebAssembly detection engine for a custom
// backend, such as the on-device Rampart NER model (see below).
backend?: SensitiveInfoBackend;
};
type ArcjetSensitiveInfoType =
| "EMAIL"
| "PHONE_NUMBER"
| "IP_ADDRESS"
| "CREDIT_CARD_NUMBER"
| "GIVEN_NAME"
| "SURNAME"
| "SSN"
| "URL"
| "TAX_ID"
| "BANK_ACCOUNT"
| "ROUTING_NUMBER"
| "GOVERNMENT_ID"
| "PASSPORT"
| "DRIVERS_LICENSE"
| "BUILDING_NUMBER"
| "STREET_NAME"
| "SECONDARY_ADDRESS"
| "CITY"
| "STATE"
| "ZIP_CODE";

Sensitive information detection runs through a detection backend – the engine that scans the request body and identifies entities. The rest of the rule (mode, allow/deny, and the result shape) is the same whichever backend you choose. For exactly what each backend detects, see Entity detection.

By default – when you don’t set a backend – the rule runs a WebAssembly engine that detects four structured types: email addresses, phone numbers, IP addresses, and credit card numbers. It runs anywhere the SDK runs, including edge runtimes, needs no extra dependencies, and adds negligible latency, so it’s the right choice for most applications. Extend it with a custom detect function when you need to match your own patterns.

For broader coverage, an optional Rampart package provides a backend powered by the on-device Rampart named-entity-recognition (NER) model. JavaScript and Python run a ~15 MB quantized ONNX model. Go embeds the same ~15 MB of weights in a nested module and infers in pure Go, with no cgo or ONNX runtime. The rule can detect names, addresses, and government or financial identifiers in addition to the built-in structured types. The model weights are bundled, so nothing is fetched at runtime and scanned text stays in your environment.

Install the package alongside your Arcjet SDK. In Python it ships as an optional extra of the arcjet package. In Go it is a nested module so the ~15 MB of model weights stay optional:

Terminal window
npm install @arcjet/sensitive-info-rampart

Then import rampart and pass it as the rule’s backend. Create the Go backend once at startup with rampart.New and reuse it. Every entity the model detects is a built-in sensitive info type (ArcjetSensitiveInfoType in JS, SensitiveInfoEntityType in Python, arcjet.EntityType in Go), so you can list the types directly in allow or deny:

main.go
package main
import (
"encoding/json"
"io"
"log"
"net/http"
"os"
"github.com/arcjet/arcjet-go"
"github.com/arcjet/arcjet-go/sensitiveinfo/rampart"
)
var backend = must(rampart.New(rampart.Options{}))
var aj = must(arcjet.NewClient(arcjet.Config{
Key: os.Getenv("ARCJET_KEY"),
Rules: []arcjet.Rule{
arcjet.SensitiveInfo(arcjet.SensitiveInfoOptions{
Mode: arcjet.ModeLive,
Deny: []arcjet.EntityType{
arcjet.SensitiveInfoEmail,
arcjet.SensitiveInfoGivenName,
arcjet.SensitiveInfoSurname,
arcjet.SensitiveInfoStreetName,
arcjet.SensitiveInfoSSN,
},
Backend: backend,
}),
},
}))
func handler(w http.ResponseWriter, r *http.Request) {
message, _ := io.ReadAll(r.Body)
decision, err := aj.Protect(
r.Context(),
r,
arcjet.WithSensitiveInfoValue(string(message)),
)
if err != nil {
log.Printf("arcjet: %v", err)
}
for _, result := range decision.Results {
log.Printf("Rule Result %+v", result)
if result.Reason.IsSensitiveInfo() {
log.Printf("Sensitive info rule %+v", result)
}
}
if decision.IsDenied() {
if decision.Reason.IsSensitiveInfo() {
http.Error(w, "Unexpected sensitive info detected", http.StatusBadRequest)
return
}
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
_ = json.NewEncoder(w).Encode(map[string]string{"message": "Hello world"})
}
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}

Without a backend, the rule continues to use the built-in engine – this package is entirely opt-in.

The full set of types the backend supports is exported as rampartEntities (JS) / rampart_entities (Python) / rampart.Entities() (Go). Use it to deny every type the backend can detect:

import { rampart, rampartEntities } from "@arcjet/sensitive-info-rampart";
sensitiveInfo({ mode: "LIVE", deny: rampartEntities, backend: rampart() });

Rampart is a compact model – a ~14.7 MB artifact (~18.5M parameters, quantized to 4-bit weights) with a 512-token context window – so it is fast enough to run inline on each request. On the Node.js CPU runtime, median (p50) inference is ~6.6 ms, dropping to ~3.9 ms p50 on WebGPU (set device: "webgpu" in the JS SDK). The Python backend runs on ONNX Runtime and defaults to CPU execution; select a different accelerator with the providers option (described later on this page). The Go backend is a nested module that embeds the same ~15 MB weights and runs inference in pure Go (no cgo). Create it once with rampart.New and reuse it. The default MaxInputChars is 4096.

On the model author’s held-out benchmark it recalls ~98% of private terms across the seven Latin-script languages it supports (English, Spanish, French, German, Italian, Portuguese, and Dutch).

Accuracy varies by input, so it’s worth knowing the trade-offs:

  • Non-Latin scripts (for example Chinese, Japanese, Korean, Arabic, Hindi, or Cyrillic) have much lower recall – the model is trained on Latin-script text.
  • Government or financial identifiers without a checksum (passports, licenses, case numbers) are recognized less reliably than the structured types the deterministic recognizers validate, such as emails and card numbers.
  • Inference runs synchronously in the request path, so this latency is added to each request the rule scans.

These figures come from the Rampart model card, which has the full per-language breakdown, calibration data, and limitations.

The rampart() factory (JS and Python) or rampart.New (Go) accepts optional configuration:

import { defaultRecognizers, rampart } from "@arcjet/sensitive-info-rampart";
rampart({
// Execution device (default: "cpu"). Set to "webgpu" to use a GPU when the
// runtime supports it.
device: "webgpu",
// Minimum confidence score for a model token to count (default: 0.5).
threshold: 0.6,
// Deterministic recognizers to run alongside the model (default:
// `defaultRecognizers`). This is the extension point for custom detection
// with this backend – add a recognizer that returns matched spans. Pass `[]`
// to rely on the model alone.
recognizers: [
...defaultRecognizers,
(value) => {
// Return an array of `{ start, end, type }` spans.
return [];
},
],
});

In JavaScript and Python, the model loads once on first use and is reused for every subsequent request. In Go, rampart.New loads and dequantizes the weights – create the backend once at startup and reuse it. It is safe for concurrent use.

In JavaScript and Python, the Rampart backend loads a native ONNX runtime and reads its bundled model weights from disk at runtime. As a result:

  • It requires a server runtime with filesystem and native-addon access. In JS that means Node.js, Bun, or Deno – it does not run on edge runtimes. In Python it runs anywhere the SDK does (Python 3.10+).
  • A server build must not bundle it, or the runtime fails to locate the model weights.

In Go, inference is pure Go. Weights are compiled into the nested module, so there is no ONNX runtime, native addon, or extra file for a bundler to miss. Applications that import the module include about 15 MB of weights in the binary.

In Python, the backend depends on ONNX Runtime (onnxruntime), which is installed for you by the arcjet[sensitive-info-rampart] extra. Model inference is synchronous and blocking, so its latency is added directly to each request the rule scans – the same is true whether you use the arcjet() or arcjet_sync() client.

For Next.js, mark the package and its native dependencies as server external packages so that Next.js loads them from node_modules at runtime instead of bundling them. Also make sure any route handler that uses the backend runs on the Node.js runtime (the default) rather than the Edge runtime:

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
serverExternalPackages: [
"@arcjet/sensitive-info-rampart",
"@huggingface/transformers",
"onnxruntime-node",
],
};
module.exports = nextConfig;
app/api/arcjet/route.ts
export const runtime = "nodejs";

Arcjet provides a single protect function that is used to execute your protection rules.

This function returns a Promise that resolves to an ArcjetDecision object. This contains the following properties:

  • id (string) – The unique ID for the request. This can be used to look up the request in the Arcjet dashboard. It is prefixed with req_ for decisions involving the Arcjet cloud API. For decisions taken locally, the prefix is lreq_.
  • conclusion (ArcjetConclusion) – The final conclusion based on evaluating each of the configured rules. If you wish to accept Arcjet’s recommended action based on the configured rules then you can use this property.
  • reason (ArcjetReason) – An object containing more detailed information about the conclusion.
  • results (ArcjetRuleResult[]) – An array of ArcjetRuleResult objects containing the results of each rule that was executed.
  • ip (ArcjetIpDetails) – An object containing Arcjet’s analysis of the client IP address. For more information, see the SDK reference.

To check whether a sensitive info rule returned a deny conclusion, use decision.isDenied() and decision.reason.isSensitiveInfo() (JS) / decision.is_denied() and decision.reason_v2.type == "SENSITIVE_INFO" (Python) / decision.IsDenied() and decision.Reason.IsSensitiveInfo() (Go).

You can iterate through the results and check whether a sensitive info rule was applied:

for (const result of decision.results) {
console.log("Rule Result", result);
}

This example logs the full result as well as the sensitive info rule:

main.go
package main
import (
"io"
"log"
"net/http"
"os"
"github.com/arcjet/arcjet-go"
)
var aj = must(arcjet.NewClient(arcjet.Config{
Key: os.Getenv("ARCJET_KEY"),
Rules: []arcjet.Rule{
arcjet.SensitiveInfo(arcjet.SensitiveInfoOptions{
Mode: arcjet.ModeLive,
Deny: []arcjet.EntityType{arcjet.SensitiveInfoEmail},
}),
},
}))
func handler(w http.ResponseWriter, r *http.Request) {
message, _ := io.ReadAll(r.Body)
decision, err := aj.Protect(r.Context(), r, arcjet.WithSensitiveInfoValue(string(message)))
if err != nil {
log.Printf("arcjet: %v", err)
}
if si := decision.Reason.SensitiveInfo; si != nil {
for _, e := range si.Denied {
log.Printf("blocked %s at [%d:%d]", e.Type, e.Start, e.End)
}
}
if decision.IsDenied() {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
}
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}

The entity types available to allow/deny depend on the active detection backend. Every type in the following list is a built-in ArcjetSensitiveInfoType, so you can list any of them regardless of backend – a type that the active backend doesn’t detect never matches. You can also detect your own types with custom entity detection.

Entity typeBuilt-in engine (default)Rampart backend
EMAIL✅✅ Model + recognizer
PHONE_NUMBER✅✅ Model (Go); model + recognizer (JS, Python)
IP_ADDRESS✅✅ Recognizer
CREDIT_CARD_NUMBER✅✅ Recognizer
URL–✅ Model + recognizer
SSN–✅ Recognizer
GIVEN_NAME–✅ Model
SURNAME–✅ Model
TAX_ID–✅ Model
BANK_ACCOUNT–✅ Model
ROUTING_NUMBER–✅ Model
GOVERNMENT_ID–✅ Model
PASSPORT–✅ Model
DRIVERS_LICENSE–✅ Model
BUILDING_NUMBER–✅ Model
STREET_NAME–✅ Model
SECONDARY_ADDRESS–✅ Model
CITY–✅ Model
STATE–✅ Model
ZIP_CODE–✅ Model
Custom✅ detect callback✅ recognizers option

For the Rampart backend, Model means the type is detected by the on-device NER model, and Recognizer means it is detected by a deterministic, validated pattern (mirroring Rampart’s deterministic redaction layer). Where the model and a recognizer overlap on the same text, the recognizer wins.

On the Go Rampart backend, phone numbers are left to the model because their digit shape overlaps with financial and government identifiers. They are not in rampart.DefaultRecognizers.

In Go, pass those types to Allow or Deny, or use rampart.Entities() for the full set. Listing a non-built-in type without a backend or a SensitiveInfoDetect callback is a configuration error.

The structured types are matched the same way by the built-in engine and by the Rampart recognizers.

Card numbers can be detected. The values 4242424242424242, 4000 0566 5566 5556, and 3782-8224-6310005 match but 4242424242424241 does not. Whether something looks like a card number is based on its initial and final digits. The initial digits determine the expected length (min, max) of the total number. The final digit is a Luhn check digit. Spaces and dashes are ignored because users often group digits.

Email addresses can be detected. The values alice@example.com and bob.smith@subdomain.example.com match but alice.example.com does not. A dotless domain (user@localhost) and a domain literal (user@[127.0.0.1], if it contains a valid IP) also match.

IP addresses can be detected. The values 127.0.0.1 and ::1 match but 012.004.002.000 does not. IP v4 and v6 addresses are supported. Whether something looks like an IP address is based on whether it parses as IpAddr from std::net.

Phone numbers can be detected. The values +1 (555) 555-5555 and (020) 334 4522 match but 555-1234 does not. As phone numbers have a wide variety of sizes and formats, special short numbers such as 911 or 14 020 are not detected. Several characters such as dots (.), dashes (-), and spaces are allowed to separate digit groups. The country code can be prefixed with plus (+) or omitted, the next region group can be in parentheses.

When using the built-in engine you can provide a custom detect function, which lets you detect entities that Arcjet doesn’t support out of the box using custom logic.

The function takes a list of tokens and must return a list of the same length. Each element is undefined if the corresponding token is not sensitive, or the name of the entity if it matches. The number of tokens that are provided to the function is controlled by the contextWindowSize option, which defaults to 1. If you need additional context to perform detections then you can increase this value.

main.go
package main
import (
"context"
"log"
"os"
"strings"
"github.com/arcjet/arcjet-go"
)
var aj = must(arcjet.NewClient(arcjet.Config{
Key: os.Getenv("ARCJET_KEY"),
SensitiveInfoDetect: func(_ context.Context, tokens []string) []arcjet.EntityType {
out := make([]arcjet.EntityType, len(tokens))
for i, tok := range tokens {
if strings.HasPrefix(tok, "sk-") {
out[i] = "API_KEY"
}
}
return out
},
Rules: []arcjet.Rule{
arcjet.SensitiveInfo(arcjet.SensitiveInfoOptions{
Mode: arcjet.ModeLive,
Deny: []arcjet.EntityType{"API_KEY"},
}),
},
}))
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}

Arcjet is designed to fail open so that a service issue or misconfiguration does not block all requests. The SDK also times out and fails open after 2000 ms by default. However, in most cases, the response time is less than 20 ms to 30 ms.

If there is an error condition when processing the rule, Arcjet returns an ERROR result for that rule and you can check the message property on the rule’s error result for more information.

If all other rules that were run returned an ALLOW result, then the final Arcjet conclusion is ERROR.

main.go
package main
import (
"io"
"log"
"net/http"
"os"
"github.com/arcjet/arcjet-go"
)
var aj = must(arcjet.NewClient(arcjet.Config{
Key: os.Getenv("ARCJET_KEY"),
Rules: []arcjet.Rule{
arcjet.SensitiveInfo(arcjet.SensitiveInfoOptions{
Mode: arcjet.ModeLive,
Deny: []arcjet.EntityType{arcjet.SensitiveInfoEmail},
}),
},
}))
func handler(w http.ResponseWriter, r *http.Request) {
message, _ := io.ReadAll(r.Body)
decision, err := aj.Protect(r.Context(), r, arcjet.WithSensitiveInfoValue(string(message)))
if err != nil {
log.Printf("arcjet: %v", err)
}
if decision.IsDenied() {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
}
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}

Arcjet runs the same in any environment, including locally and in CI. You can use the mode set to DRY_RUN to log the results of rule execution without blocking any requests.

We have an example test framework you can use to automatically test your rules. Arcjet can also be triggered based using a sample of your traffic.

For details, see the Testing section of the docs.