Make regulated data safe to use with LLMs.
Anonymize before the model sees it. Reveal only where it's allowed.
A developer toolkit for building copilots, RAG, and agents over healthcare, finance, and enterprise data.
anonde.io · Live demo · Quickstart · Benchmarks
Your input (text, JSON, NDJSON, logs, or PDF; scanned PDFs are OCR'd automatically):
From: sarah.chen@acme.example
Hi, this is Sarah Chen (+1 415 555 0142). Card 4111-1111-1111-1111
was charged twice on 2024-03-15 for $89.99, please refund.
What the LLM sees:
From: <EMAIL_ADDRESS_1>
Hi, this is <PERSON_1> (<PHONE_NUMBER_1>). Card <CREDIT_CARD_1>
was charged twice on <DATE_TIME_1> for $89.99, please refund.
What your user sees: the original text, restored inside your trust boundary, gated by actor + purpose audit metadata. Tokens are stable per (tenant, doc) and reversible via an in-memory vault you control.
- Drop-in for any LLM workflow. Same shape in, same shape out. Plug it between your app and OpenAI, Anthropic, Bedrock, Ollama, or your own model. They see only tokens.
- Wins on leak rate. anonde-ner has the lowest leak rate on all 29 gold-annotated corpora the bench tracks across English, German, Spanish, French and Italian — covering clinical, legal, finance, structured PII, and adversarial / out-of-distribution text. (full results)
- Local-first. Ships as a Go library or a Docker image you run yourself. No cloud calls. NER models are baked into the image, so there is no outbound HuggingFace traffic at request time.
- Multilingual. Open-set NER (GLiNER) plus 70 region-aware pattern recognizers covering international IDs and a dozen-plus national jurisdictions.
- Reversible, audited. Tokens map back to cleartext only where you allow it. The reveal call requires
actor+purposeand is the only place plaintext comes back. - Recall-biased. Missing a span is a leak; tokenising one too many is cheap. The bench tracks this explicitly via
leak_rate(lower is better).
Two ways to run anonde — pick the one that matches how you ship.
One command, no Go toolchain needed. The patterns-only image is ~12 MB and cold-starts in <1s; the NER image (~770 MB) bakes in GLiNER + libonnxruntime for PERSON / ORG / LOC detection.
# Patterns-only (no model download, no CGO)
docker run --rm -p 8081:8080 ghcr.io/anonde-io/anonde:latest
# NER (GLiNER + libonnxruntime baked in)
docker run --rm -p 8081:8080 ghcr.io/anonde-io/anonde-ner:latestThen anonymize a request:
curl -sS -X POST http://localhost:8081/v1/anonymizations \
-H "Content-Type: application/json" \
-d '{"tenant_id":"demo","content":"Hi, this is Sarah Chen (sarah.chen@acme.example)."}'
# → { "id": "anon_8f3c…", "anonymized_content": "...", "tokens": [...] }Image variants, port/listen-address overrides, persistent volumes, and docker compose profiles are all in docs/DEPLOYMENT.md.
The NER image runs open-set GLiNER, so the list of entity labels is
supplied at inference time — anonde ships four curated label sets and
selects one with the GLINER_LABEL_SET env var. chat is the default;
the others are opt-in.
| Set | Tuned for | Highlights |
|---|---|---|
chat (default) |
Casual / conversational traffic | Names, org, email/phone/URL, postal geography, structured financial + government IDs. Drops age, profession, job title, date / date of birth, and the clinical / German-insurance labels — they over-redact ordinary chat ("18 years of experience" → AGE, "tech" → PROFESSION). |
clinical |
Clinical / HIPAA de-identification | The full default set: everything in chat plus age, profession, date / date of birth, patient/doctor/hospital labels, and the German insurance / tax / case-file IDs (Versicherungsnummer, Steuer-Identifikationsnummer, Aktenzeichen, …). |
finance |
Bank statements, KYC, payments, tax forms | Identity + contact core plus bank account / routing numbers, IBAN, SWIFT/BIC, credit-card number + CVV, tax IDs (SSN / ITIN / EIN / Steuer-ID), and account / transaction identifiers. |
legal |
Pleadings, contracts, court filings, matter files | Identity + geography core, keeps date / date of birth (legal docs are date-sensitive, unlike chat), plus case / docket / matter / contract / bar numbers, court name, and party roles (attorney, plaintiff, defendant, judge). |
# Default (chat) — no env needed
docker run --rm -p 8081:8080 ghcr.io/anonde-io/anonde-ner:latest
# Clinical / HIPAA coverage (adds AGE / DATE + clinical labels)
docker run --rm -p 8081:8080 -e GLINER_LABEL_SET=clinical ghcr.io/anonde-io/anonde-ner:latest
# Finance (bank / IBAN / SWIFT / card+CVV / tax IDs)
docker run --rm -p 8081:8080 -e GLINER_LABEL_SET=finance ghcr.io/anonde-io/anonde-ner:latest
# Legal (case / docket / bar numbers + dates kept)
docker run --rm -p 8081:8080 -e GLINER_LABEL_SET=legal ghcr.io/anonde-io/anonde-ner:latestAll four sets map onto the same canonical entity types the pattern
recognizers emit (PERSON, ORGANIZATION, IBAN_CODE, US_BANK_NUMBER,
ID, …), so anonymizer operators and reveal/detokenize behave identically
regardless of which set is active. An unrecognised value falls back to
chat. Go-library callers set GLiNERConfig.Labels / LabelToEntity
directly (e.g. recognizers.FinancePIILabels).
go get github.com/anonde-io/anondepackage main
import (
"context"
"fmt"
"github.com/anonde-io/anonde"
"github.com/anonde-io/anonde/analyzer"
"github.com/anonde-io/anonde/anonymizer"
"github.com/anonde-io/anonde/anonymizer/operators"
)
func main() {
text := `Hi, I'm Sarah Chen (sarah.chen@acme.example, +1 415 555 0142). Card 4111-1111-1111-1111.`
engine := anonde.DefaultAnalyzerEngine()
results, _ := engine.Analyze(context.Background(), text, analyzer.AnalysisConfig{
Language: "en",
ScoreThreshold: 0.3,
RemoveConflicts: true,
})
anon := anonde.DefaultAnonymizerEngine()
out, _ := anon.Anonymize(text, results, anonymizer.AnonymizerConfig{
"*": &operators.Replace{}, // → <PERSON_1>, <EMAIL_ADDRESS_1>, <PHONE_NUMBER_1>, <CREDIT_CARD_1>
})
fmt.Println(out.Text)
}Default build is pure Go, no CGO. The -tags ner build enables in-process GLiNER NER; see docs/DEPLOYMENT.md.
The same server speaks three transports on one port:
- REST/JSON via grpc-gateway:
POST /v1/anonymizations,POST /v1/anonymizations/{id}/reveal|detokenize,DELETE /v1/anonymizations/{id}?tenant_id={tenant_id},POST /v1/synthesize,GET /v1/version.idis optional on create (server mintsanon_<hex>if omitted); tenant lives in the request body / query for now and moves to a bearer-token header when auth lands. JSON fields are snake_case on the wire (tenant_id,content_format,anonymized_content, …); inputs also accept the camelCase form so generated gRPC clients work without translation. - Connect (Connect/JSON, Connect/Protobuf, gRPC-Web):
POST /anonde.v1.Service/<Method>. - Native gRPC over HTTP/2 cleartext: same
/anonde.v1.Service/<Method>path.
Two optional surfaces ride alongside: PDF redaction (POST /v1/anonymizations/pdf, see PDFs & scans) and an OpenAI-compatible proxy (POST /v1/chat/completions, see OpenAI proxy).
Source of truth: proto/anonde/v1/anonde.proto. Regenerate handlers with buf generate. Full round-trip examples (text, JSON, PDF) live in docs/QUICKSTART.md.
PDFs are a first-class HTTP endpoint, not a separate binary. Two surfaces:
- Text PDFs through the normal endpoint — send a base64 PDF with
content_format: "pdf"toPOST /v1/anonymizations; the text layer is extracted and run through the same analyzer pipeline as text input. - Raw PDF in, redacted PDF out via
POST /v1/anonymizations/pdf— returns a PDF with black boxes over each PII span; reversible viaGET /v1/anonymizations/{id}/reveal-pdf. Opt-in withANONDE_PDF_ENABLED=1.
When the text layer is empty or too short (an image-only scan or
photo-to-PDF), both surfaces transparently rasterise each page and OCR it
before running the analyzer — no caller change. The anonde-ner image
bundles poppler-utils + tesseract-ocr
(eng+deu+fra+spa+ita+ron), so OCR and the YOLOS signature redactor are on
by default there; the patterns-only image stays ~12 MB and skips them.
Per-request knobs (mode, operator, entities, score-threshold, ocr-langs, …) bind from URL query params. Full flows, the field table, and OCR env vars are in docs/DEVELOPER_GUIDE.md and docs/DEPLOYMENT.md.
The lowest-friction integration: point your existing OpenAI SDK at anonde
instead of api.openai.com. anonde anonymizes the prompt, forwards it to the
real provider, de-anonymizes the response, and hands it back in OpenAI shape.
from openai import OpenAI
# Server started with ANONDE_OPENAI_BASE_URL + ANONDE_OPENAI_API_KEY set.
client = OpenAI(base_url="http://localhost:8081/v1", api_key="unused")
resp = client.chat.completions.create(
model="openai/gpt-4o",
messages=[{"role": "user", "content": "Email a summary to sarah.chen@acme.example"}],
)
# The provider only ever saw <EMAIL_ADDRESS_…>; the reply has the real address restored.Works with the raw OpenAI SDK, LangChain, or anything that speaks the OpenAI
API. Provider is selected in-band by a provider/model prefix. Env vars,
multi-provider routing, and the v0.1 streaming limitation are in
docs/OPENAI_PROXY.md.
Extensibility is the part Presidio gets right and we copied. A pattern recognizer is a regex, a label, a language list, and optional context words that boost the score when they appear nearby. Add one two ways:
- In-repo — drop a file into
analyzer/recognizers/and register it inanonde.go; it joins every engine by default. - As a library consumer (no fork) — implement
analyzer.EntityRecognizerandengine.Registry.Add(...)at startup.
Either path joins the parallel-dispatch pipeline and the conflict resolver handles overlap automatically. Worked examples (context boosts, both paths), the 70-recognizer catalogue, and how to add a model-backed NER recognizer are in docs/RECOGNIZERS.md. NER preferences and the full pipeline rules are in docs/ARCHITECTURE.md.
- Healthcare. Chart summaries, discharge letters, clinical Q&A. Keep PHI off the wire to third-party models.
- Finance. KYC review, support triage, statement summarisation. Account numbers, card data, and PII stay inside your boundary.
- Logs & telemetry. Application logs, audit trails, SIEM exports, and traces often carry emails, IPs, account IDs, and free-text from users. Run them through anonde before they hit a remote LLM, a log aggregator, or a BI store.
- Enterprise. Internal copilots over support tickets, contracts, HR docs. Audit who reveals what, and why.
Want to see the full flow in the browser? anonde.io.
- Quickstart: local round-trip via HTTP
- API reference (Swagger) — browsable spec auto-generated from
proto/anonde/v1/anonde.proto:- Hosted: https://anonde-io.github.io/anonde/ (redeployed on every
mainpush that touches the proto or generated spec; needs Pages enabled in repo settings → sourceGitHub Actions) - Local: open
docs/api/index.htmlaftermake proto(serve over HTTP, e.g.python3 -m http.server) - Source JSON:
gen/anonde/v1/anonde.swagger.json
- Hosted: https://anonde-io.github.io/anonde/ (redeployed on every
- Developer guide: text + PDF + scanned-image flows, per-request PDF knobs, Prometheus metrics
- Recognizers: 70-recognizer table and writing custom recognizers
- Architecture: pipeline, directory tree, conflict resolution
- Operators: Replace, Redact, Mask, Hash, Encrypt, Synthesize
- OpenAI proxy: point an OpenAI SDK at anonde
- Deployment: Docker, env vars, ports, persistence, PDF/OCR, CI
- Telemetry: what the anonymous heartbeat sends, and how to disable it
- Benchmark matrix: full results
anonde sends an anonymous heartbeat once every 24 hours (deployment shape,
backend mix, entity-type counts) so we can prioritise the roadmap against
real signal. No input/output text, token values, vault contents, IPs,
hostnames, tenant/doc IDs, actors, or purposes are ever sent. Disable with
ANONDE_TELEMETRY=off or ANONDE_OFFLINE=1. The full field list and the wire
payload are in docs/TELEMETRY.md.
- Issues: bug reports, feature requests, and questions all welcome. The repo has issue templates for each.
- Pull requests: start with CONTRIBUTING.md for dev setup, recognizer-adding patterns, and bench expectations. No DCO or CLA; Apache 2.0 §5 grants the inbound license automatically.
- Code of conduct: Contributor Covenant 2.1.
Conduct concerns go to
conduct@anonde.io. - Security: vulnerabilities go through the private channels in SECURITY.md, not public issues.
Apache 2.0. See NOTICE for the attribution
notice that downstream redistributors are asked to preserve.