PII Guard
- What it does. An OpenAI-compatible gateway for AI agents. Change one base URL, and the names, addresses, IDs and faces it detects become reversible placeholders (
[[PERSON_7f3a]]) before the request leaves. The real values come back in the answer, mid-stream and inside tool calls. Detection is rules plus a small NER that runs on CPU: XLM-RoBERTa base, distilled from Meta's Muse Glimmer 30B. - How well. On 990 fictional agent-traffic documents (de, en, hu), 0.2% of personal values reached the model and 0.7% of masks hit text that was not personal data. The lowest-leak alternative, PrivAiTe, leaked 2.4% and over-masked 35.2% on the same documents.
- How fast. The NER takes 43 ms per message on a laptop CPU (p50; p95 50 ms, measurement). No LLM call and no API cost in fast mode.
- Where your data goes. In fast mode, the default (rules plus the on-device NER, no LLM call), the original text is processed only on your machine. The hosted demo receives what you type, so use fictional data there. The optional audit and SAM 3.1 endpoints receive the original text or pixels. Anything the detectors miss passes through: pseudonymization, not anonymization or a compliance guarantee.
990 fictional documents (de, en, hu), drawn by pii-guard eval report from the run records. All sets · Shareable PNGs
Built for the Meta Global AI Developer Hackathon (Oct 26 – Nov 4, 2026) · 3-minute video walkthrough. Version 0.1.0, installed from git; no tagged release yet. Known gaps.
Quickstart
Walkthrough: watch the 3-minute demo video on YouTube.
No install: the live demo runs as a guest with no sign-up (fictional data only; capped sponsored allowance).
On your machine (needs uv): the API and the gateway on http://127.0.0.1:8787, with a mock model that echoes what it receives:
uvx --python 3.12 \
--from 'wattmind-pii-guard @ git+https://github.com/watt-mind/pii-guard' \
pii-guard up
Launch a coding agent directly (OpenCode, Codex, Claude Code, pi, Muse Code) behind an in-process local gateway with Meta's muse-spark-1.3:
export META_API_KEY=<your-key>
uvx --python 3.12 \
--from 'wattmind-pii-guard @ git+https://github.com/watt-mind/pii-guard' \
pii-guard run opencode --upstream meta --model muse-spark-1.3
- First run: downloads PyTorch and the NER once (the NER is 1.1 GB);
/healthzreports"ner": "ready"when detection is live. - A real model:
--upstream metawithMETA_API_KEY;openai,fireworks,openrouteror any--upstream-urlwork the same way (pii-guard upoptions, coding agent integrations). - No auth by default, bound to 127.0.0.1: set
PII_GUARD_API_TOKENbefore exposing it (how to expose it safely).
Results
Table as text
| Leak · over-masking | PII Guard fast | PrivAiTe | Presidio | LLM Guard |
|---|---|---|---|---|
| Hand-written · 62 docs (de+en+hu+pl) | 0.0% · 1.3% | 13.0% · 25.7% | 25.5% · 26.1% | 46.3% · 33.7% |
| Agent traffic · 990 docs (de+en+hu) | 0.2% · 0.7% | 2.4% · 35.2% | 16.6% · 33.4% | 20.2% · 16.9% |
| Nemotron-PII · 300 docs (en) | 0.6% · 1.5% | 4.0% · 5.6% | 12.8% · 32.0% | 28.8% · 0.6% |
| CEE-PII · 451 docs (en+hu) | 0.8% · 7.3% | 4.4% · 59.2% | 36.5% · 52.5% | 75.8% · 66.4% |
| Gretel finance · 400 docs (de+en) | 19.1% · 24.4% | 27.7% · 46.5% | 18.8% · 75.4% | 59.3% · 17.0% |
Leak: labeled personal values left fully readable in what the model receives. A value with only part left readable (a surname, the tail of a number) is a partial leak, counted separately: PII Guard fast 0.0% on agent traffic, 1.4% on the hand-written set. Over-masking: masks on text that was not personal data. Lower is better for both; in each row, the lowest leak and the lowest over-masking are bold.
Caveats
- The sets are small (tens to hundreds of documents per language): a point or two of difference is noise. Agent traffic is written by Muse models from fictional seeds, which can flatter a detector taught by Muse labels; that is why the hand-written and public sets sit next to it.
- Finance text is the weak spot. On Gretel finance's own labels PII Guard leaks 19.1%, about level with Presidio's 18.8%; on corrected labels (why) 2.4% against 14.8%. Over-masking is high on Gretel finance (24.4%) and CEE-PII (7.3%), where reference numbers without a cue word come out as IDs.
- NER v4.2 leaks more than the previous v2.0 on CEE-PII (0.8% against 0.4%), the Polish pl-pii-bench (4.2% against 3.5%) and TAB court text (5.1% against 4.0%, scored locally only), and less on the hand-written sets, agent traffic and the EU multilingual set (model notes).
How this is measured
Every tool ran on the same documents (byte-identical files) and was scored by the same code; comparison tools use their documented defaults (configurations). All data is fictional. PII Guard fast is rules plus the published NER v4.2 (model card). The hand-written set pools de, en, hu, pl; the full tables, per-language numbers, Ai4Privacy (in-distribution for the NER, so not a win) and the corrected finance labels are in the benchmark report.
NER model time per message on CPU (the model alone, not the whole gateway): p95 50 ms (122 messages of about 382 characters, arm64 macOS 26.5, 8 threads; measurement).
Records behind the PII Guard fast column: Hand-written (1 · 2), Agent traffic, Nemotron-PII, CEE-PII, Gretel finance; every other cell is in the run log.
Per-language results, the open NER models compared behind the same rules, and latency are in the benchmark report.
Which Meta models do what
Muse Glimmer 30B was the best zero-shot detector we measured (F1 99.3, 0.0% leak on the 38
Hungarian and English hand-written documents of that run,
record) and far too slow
for a request path at 9.4 s per document (p50). So it labeled the training text instead, and
XLM-RoBERTa base learned from it: 43 ms on CPU, published as
watt-mind/pii-guard-ner-xlmr-base v4.2.
In the default fast mode, every model that sees original personal data runs on your machine. Audit mode (Muse Spark Open) and SAM 3.1 are opt-in and receive originals at the endpoint you configure.
| Job | Meta model |
|---|---|
| Fast-mode NER (CPU) | XLM-RoBERTa base (FacebookAI, MIT), fine-tuned: model card, training |
| Teacher: labels training text | Muse Glimmer 30B (open weights, Apache-2.0): distillation |
| Benchmark writer; demo model | Muse Spark (Meta Model API): which model does what |
| Audit mode, red team | Muse Spark Open (open weights): audit runs, red team |
| Fictional ID specimens | Muse Image: painted the portraits, signatures and stamps (generation record) |
| Signatures in images | SAM 3.1 (Meta Model API, decoded with Meta's meta_sam_parser); faces stay on the device (YuNet): specimen results |
Inside the gateway
The gateway speaks the OpenAI Chat Completions and Responses APIs. It masks messages, tool definitions, tool-call arguments, tool results and images, restores placeholders in streams, keeps one placeholder per person per session, and fails closed on anything it can't mask; the mapping never leaves PII Guard. Every document in the hand-written set unmasks back to its original exactly.
| Mode | Detection | Leaves your machine |
|---|---|---|
fast (default): everyday traffic |
Rules + on-device NER; no LLM call | Only the masked request, to your LLM provider |
fast with PII_GUARD_DETECTORS=rules: offline runs, CI, structured data |
Rules only; free-text names only as known values your app registers | Only the masked request |
audit (opt-in, X-PII-Guard-Mode: audit): measuring the upper bound, training labels |
Rules + NER + an LLM detector; fails closed if that call fails | The original text, to the audit endpoint you configure |
Grammar-aware. In Hungarian a name changes with every case; the ending stays outside the placeholder:
| Hungarian (real output), with an English gloss | |
|---|---|
| You send | Kérem, írjon Próba Zsuzsannának a zsuzsa.proba@example.com címre. Please write to Zsuzsanna Próba at … |
| The model sees | Kérem, írjon [[PERSON_968e29]]nak a [[EMAIL_e7038b]] címre. … to [[PERSON]] … |
| You get back | Megírtam a levelet Próba Zsuzsannának. I wrote to Zsuzsanna Próba. |
Language packs. Hungarian, English, German and Polish each ship validated identifier rules, cue words and case handling; a new language is one package (how to add a language).
What it detects: people, addresses, emails, phone numbers, dates of birth, national and document IDs (including Hungarian tax and TAJ numbers), bank accounts and payment cards, vehicle plates, parcel numbers and secrets such as API keys, plus faces, ID photos and signatures in images (full entity list).
Real agents behind it. On 2026-10-29 four unmodified coding agents (pi, OpenCode, Codex, Muse Code) worked a fictional customer file through the gateway: 115 requests, none of the file's 9 personal values reached the model, and the answers and the files on disk came back with the real values. No turn was refused; in an earlier run on 2026-10-28, 7 of 15 were, before two gateway fixes (a system note saying the placeholders are intentional, and the NER no longer masking file names). Two Codex turns stopped on gateway bugs; ten turns is a sample, not a benchmark (harness matrix).
Architecture · sessions and modes · threat model · GDPR context
Thirteen differentiators, each with evidence.
Use it from code
Round trip
Mask, then unmask with the session PII Guard keeps. With pii-guard up running, this script
takes the session id and the person placeholder from the first response (output from fast mode):
R=$(curl -sS http://127.0.0.1:8787/v1/mask \
-H 'Content-Type: application/json' \
-d '{"text": "Email Jane Placeholder, jane.placeholder@example.net, about the refund.", "language": "en"}')
echo "$R"
# {"masked_text":"Email [[PERSON_604260]], [[EMAIL_84bb25]], about the refund.",
# "session_id":"s_a925963eb8819a782a68c56f41265424","entities":[...],"kept":[]}
SID=$(echo "$R" | python3 -c 'import json,sys; print(json.load(sys.stdin)["session_id"])')
P=$(echo "$R" | grep -o '\[\[PERSON_[0-9a-f]*\]\]' | head -1)
curl -sS http://127.0.0.1:8787/v1/unmask \
-H 'Content-Type: application/json' \
-d "{\"text\": \"Done: I emailed $P about the refund.\", \"session_id\": \"$SID\"}"
# {"text":"Done: I emailed Jane Placeholder about the refund.","unresolved":[]}
The session id and the placeholders differ on every run: each new session gets its own.
Python client
Put it in front of an existing client; only the base URL changes:
# pip install openai
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8787/v1", api_key="unused") # mock upstream
stream = client.chat.completions.create(
model="mock",
stream=True,
messages=[
{"role": "user", "content": "Refund Jane Placeholder, jane.placeholder@example.net."}
],
)
for chunk in stream: # the mock echoes the masked text it got; the gateway restores the values
print(chunk.choices[0].delta.content or "", end="")
# Echo: Refund Jane Placeholder, jane.placeholder@example.net.
What the model actually received: curl -sS 'http://127.0.0.1:8787/v1/traffic?limit=1' shows
the last gateway request as sent upstream, with placeholders (pii-guard up keeps a masked-only
traffic log). API guide ·
agents and harnesses.
Console tour
Console tour, recorded in fast mode (rules + NER) with fictional data. More screens · Video walkthrough.
Run the console locally: make demo from a checkout.
Contributing, security, license
The contributing guide has the rule that matters most: never commit real personal data, not even in a test.
Security: found a way to make personal data leak past PII Guard? Report it privately: SECURITY.md.
Licensed Apache-2.0.
Metadata
Release files for wattmind-pii-guard 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| wattmind_pii_guard-0.1.0.tar.gz | 19.7 MB | Without provenance | Details for wattmind_pii_guard-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| wattmind_pii_guard-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for wattmind_pii_guard-0.1.0-py3-none-any.whl |
Total release size: 23.6 MB
Release files / wattmind_pii_guard-0.1.0.tar.gz
| Download URL | wattmind_pii_guard-0.1.0.tar.gz |
|---|---|
| Size | 19.7 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0aa979638b8effc9ecf7315739cfbfb52c007b5207ad044e086ccf8774b9ca7a
|
|
BLAKE2b-256 checksum How to use checksums |
9a4ec99be6b18e219efa1f9165040e203c1b380cc357fa9732a0448552dbb28f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / wattmind_pii_guard-0.1.0-py3-none-any.whl
| Download URL | wattmind_pii_guard-0.1.0-py3-none-any.whl |
|---|---|
| Size | 3.8 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0a7486314a50f887313a069d28febaa02f1f4609931925eba840626d920afea0
|
|
BLAKE2b-256 checksum How to use checksums |
c5f0dfd86d24d2a6c58303b8141171f4ca7b15a7d381b8299d22f00687aeb315
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for wattmind-pii-guard 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.