Skip to content

AI data loss prevention for Go

People often paste personal data into AI prompts – card numbers, phone numbers, home addresses, and whole résumés. Once that data reaches your AI provider, the provider’s logging and retention apply to it, not yours.

Arcjet sensitive info detection scans prompt content inside your application, before it reaches the AI provider. Detection runs locally in your own environment, so the raw text doesn’t leave your app. The SDK reports the decision to Arcjet with the type and position of each finding, but not the matched text. When Arcjet finds something, you decide what happens next: block the request, remove the data, or ask the user to edit their prompt.

Install the SDK and the Rampart backend:

Terminal window
go get github.com/arcjet/arcjet-go github.com/arcjet/arcjet-go/sensitiveinfo/rampart

Rampart runs on-device and detects email addresses, phone numbers, IP addresses, card numbers, names, addresses, and government identifiers. For the full list, see On-device detection with Rampart.

main.go
package main
import (
"encoding/json"
"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.SensitiveInfoCreditCardNumber,
arcjet.SensitiveInfoEmail,
},
Backend: backend,
}),
},
}))
type chatRequest struct {
Message string `json:"message"`
}
func chat(w http.ResponseWriter, r *http.Request) {
var body chatRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
decision, err := aj.Protect(
r.Context(),
r,
arcjet.WithSensitiveInfoValue(body.Message),
)
// Fail closed when Arcjet can't be reached.
if err != nil {
log.Printf("arcjet: %v", err)
http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
return
}
if decision.IsDenied() {
if decision.Reason.IsSensitiveInfo() {
http.Error(w, "Sensitive information detected – remove it from your prompt", http.StatusBadRequest)
return
}
// Block other denials too, such as one from a remote rule.
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// Fail closed when Arcjet couldn't complete the check.
if decision.IsErrored() {
log.Printf("arcjet: %v", decision.Err())
http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
return
}
_ = json.NewEncoder(w).Encode(map[string]string{"reply": "..."})
}
func main() {
http.HandleFunc("/chat", chat)
log.Fatal(http.ListenAndServe(":8000", nil))
}
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}

Sensitive info detection runs on-device with Rampart, a named-entity-recognition (NER) model paired with deterministic recognizers:

  • Deterministic recognizers match structured types with validated patterns – card numbers, email addresses, IP addresses, URLs, and Social Security numbers.
  • The NER model detects the free-form PII that people paste into prompts – names, street addresses, phone numbers, and government or financial identifiers. These don’t follow a fixed pattern, so a model finds them where pattern matching can’t.

The SDK rule runs detection on your own infrastructure, so the text isn’t sent to Arcjet or a third party for analysis. That makes it a good fit in front of an AI provider. The server-side sensitive information detector in an agent guard policy and coding agent hooks screen text on Arcjet’s side instead. Use the server-side detector for text longer than the SDK scans, and to detect protected health information (PHI) and secrets such as API keys, which on-device detection doesn’t cover.

Use deny to list the entity types to block, or allow to block everything except the types you list (the two are mutually exclusive). Tune the list to your app – for a support bot that legitimately collects phone numbers, leave PHONE_NUMBER out of deny:

arcjet.SensitiveInfo(arcjet.SensitiveInfoOptions{
Mode: arcjet.ModeLive, // Blocks requests. Use ModeDryRun to log only.
Deny: []arcjet.EntityType{
arcjet.SensitiveInfoCreditCardNumber,
arcjet.SensitiveInfoEmail,
},
Backend: backend, // Create once with rampart.New
})

For every entity type and where Arcjet detects it, see Entity types.

The Rampart NER model detects names, addresses, and government or financial identifiers. Add them to Deny. Create the backend once at startup with rampart.New:

main.go
package main
import (
"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.SensitiveInfoGivenName,
arcjet.SensitiveInfoSurname,
arcjet.SensitiveInfoEmail,
arcjet.SensitiveInfoSSN,
},
Backend: backend,
}),
},
}))
func handler(w http.ResponseWriter, r *http.Request) {
message := r.FormValue("message")
decision, err := aj.Protect(r.Context(), r, arcjet.WithSensitiveInfoValue(message))
// Fail closed when Arcjet can't be reached.
if err != nil {
log.Printf("arcjet: %v", err)
http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
return
}
if decision.IsDenied() {
if decision.Reason.IsSensitiveInfo() {
http.Error(w, "Please remove personal information", http.StatusBadRequest)
return
}
// Block other denials too, such as one from a remote rule.
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// Fail closed when Arcjet couldn't complete the check.
if decision.IsErrored() {
log.Printf("arcjet: %v", decision.Err())
http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
}
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}

The model weights ship with the package (about 15 MB), so nothing is fetched at runtime. Inference is pure Go and runs on the request path. Rampart scans up to MaxInputChars characters, 4,096 by default, and the rule denies a longer message without scanning it. Phone numbers are left to the model, because their digit shape overlaps with financial and government identifiers. For options and the full entity list, see the Rampart reference.

Pass the text to scan as sensitiveInfoValue in JavaScript, sensitive_info_value in Python, or WithSensitiveInfoValue in Go. For a chat endpoint, this is usually the user’s most recent message. To scan every message, pass the full conversation history instead – PII can appear earlier in the exchange.

Set mode to "DRY_RUN" in JavaScript, Mode.DRY_RUN in Python, or ModeDryRun in Go to log detections without blocking requests. Run it in production to see which PII appears in your prompts, then switch to live mode when the entity list matches what you want to block.

Sensitive info detection controls what data reaches your AI provider. Pair it with the other AI protection layers, each of which covers a different risk: