Compare approaches
Four ways teams keep personal data out of an LLM prompt. This page compares the approaches, not vendors - every zer0pii claim below cites the code or doc it comes from; the other columns state only general properties of that approach.
Compare approaches
| zer0pii (masking gateway) | Provider-side data controls | Generic DLP | DIY regex | |
|---|---|---|---|---|
| Does the provider ever see raw PII? | No - detected entities are replaced with placeholder tokens before the request leaves the gateway, and restored in the response. Source: apps/gateway/core/pipeline.py, apps/gateway/core/token_vault.py | The provider does see the raw text; the control is a contractual or configuration promise (zero-retention terms, an enterprise data-processing agreement) about what happens to it afterward, not a technical barrier before the call. | Usually inspects traffic to detect or block a pattern; it does not rewrite the payload, so if the request is allowed through, the provider sees the raw content. | Only whatever the regex actually matches is removed before sending; anything the pattern misses reaches the provider raw. |
| Does the model still get useful context? | Yes - placeholders preserve sentence structure and entity type, and are restored in the reply, so the model reasons over the same shape of text. Source: apps/gateway/core/reconstitutor.py | Yes, always - since the model receives the unmodified text. | Not applicable in the same sense - a block/alert tool does not rewrite the payload, so context is either fully present (allowed) or the request never proceeds (blocked). | Depends on the replacement chosen; a blunt find-and-delete regex can strip words the model needed, or leave gaps that read oddly. |
| Names, organizations, places (context-dependent entities) | Semantic tier (NER) catches these when it fires; measured recall varies by entity and language, and is not perfect. Source: docs/DETECTION-EVAL.md (NER tier2: PERSON 76.2%, LOCATION 86.8%, ORGANIZATION 51.4% on the 1,500-row sample) | Not addressed by this approach at all - it does not detect or redact anything, since it only governs what the provider does with whatever it receives. | Weak on this class by design: DLP pattern libraries are built around structured identifiers (card numbers, ID formats), not free-text names or organizations, which do not have a fixed shape to match. | Effectively unable to catch these reliably - a name or place has no fixed pattern, so a regex either misses most of them or over-matches common words. |
| Checksum-valid IDs (cards, national IDs, IBANs) | Deterministic tier validates structured identifiers against their checksum (Luhn, mod-97, etc.) before treating a match as real. Source: apps/gateway/core/deterministic.py; docs/DETECTION-EVAL.md (e.g. IBAN 99.92% recall against gold) | Not addressed - this is a detection capability the provider-side control does not have. | Often supported - checksum validation for common identifier formats is a standard DLP pattern-library feature. | Possible to hand-write, but each checksum algorithm is its own bug surface, and coverage is only as complete as the identifiers someone thought to add. |
| Streaming responses restored correctly? | Yes - a dedicated reconstitutor handles token boundaries that split a placeholder across arbitrary SSE chunks, without buffering the full response. Source: apps/gateway/core/reconstitutor.py (StreamReconstitutor) | Not applicable - nothing is masked, so there is nothing to restore in a stream. | Not applicable to LLM response streams in the way it applies to network traffic; most DLP tooling targets file transfer and email, not token-by-token model output. | Hard to get right - a hand-rolled scan-and-replace over a stream has to handle a match split across two chunks, which is easy to miss and easy to get subtly wrong. |
| Code blocks left intact? | Yes by design - markdown code blocks, variable names, package names and AST syntax are excluded from masking unless a hardcoded credential is confirmed. Source: CLAUDE.md (Core Principle 4, CODE RESILIENCE); apps/gateway/core/ast_filter.py | Not applicable - nothing is rewritten, so code is never at risk of corruption from a redaction pass. | Depends on the tool; a pattern scanner not written for source code can flag or mangle look-alike strings (a hex constant, a fake-looking test fixture) inside code. | A real risk - a general-purpose regex tuned for prose will often match inside code (a UUID literal, a phone-shaped test constant) and break it. |
| Proof of what happened (signed receipts)? | Yes - each request is attested with an Ed25519 signature over a hash of the raw and sanitized payloads, verifiable independently of zer0pii. Source: apps/gateway/core/attestation.py (Attestor.sign, verify) | Only whatever the provider's own logs or compliance reports state; no independently verifiable, per-request cryptographic proof is a standard feature of this approach. | Usually an internal log entry in the DLP vendor's own console, not a portable, independently verifiable artifact. | None unless someone builds it - a homegrown script has no attestation layer unless it is added deliberately. |
| Latency cost | Deterministic-only path: p50 under 10ms, p99 under 20ms. Semantic (NER) path: p50 under 300ms for a full pasted page, and it fails open to the deterministic path if it would exceed that budget. Source: CLAUDE.md (LATENCY BUDGET, PER PATH) | None beyond the provider's own response time - there is no separate scanning step in front of the call. | Varies by deployment (inline network inspection adds some latency; out-of-band monitoring adds none to the request itself), but is not a number this page can state without a specific product in mind. | Depends entirely on the implementation; a naive large regex set run per request can be slow, but there is no fixed number, since it is whatever code someone writes. |
| Setup effort | Point the SDK base URL at the gateway host and add the API key - the request/response shape is unchanged otherwise. Source: apps/web (integrations/docs pages: base URL + header swap pattern) | Usually a contract or account-tier change (enterprise terms, a data-processing addendum) rather than a code change. | Typically an infrastructure deployment: an agent, a proxy, or a network tap, plus policy authoring - more operational surface than a client-side change. | Fast to start, since it is just a script - but every entity type, edge case and checksum has to be written and maintained by hand. |
| What it does NOT cover | PASSWORD recall is 47.96% (fundamentally low-context strings) and PHONENUMBER recall is 84.41% on the gold eval; the semantic tier degrades to deterministic-only when it would exceed its latency budget; and Cursor and Claude Code's subscription-seat mode are not coverable (no base-URL override / no key to proxy for a subscription seat). Source: docs/DETECTION-EVAL.md (lines 64-65); CLAUDE.md (LATENCY BUDGET fail-open); docs: Developer tools (dev-tools.md) | Does not detect or redact anything at all - it is a policy about the provider's handling of whatever it receives, not a technical control on the data itself. | Free-text, context-dependent entities (names, organizations, addresses) with no fixed shape to match; also generally not designed for token-level LLM streaming output. | Anything the author did not anticipate - new formats, new locales, streaming, checksum validation, and code-safety are all separate problems a regex-only approach does not solve on its own. |
Does the provider ever see raw PII?
- zer0pii (masking gateway)
- No - detected entities are replaced with placeholder tokens before the request leaves the gateway, and restored in the response.Source: apps/gateway/core/pipeline.py, apps/gateway/core/token_vault.py
- Provider-side data controls
- The provider does see the raw text; the control is a contractual or configuration promise (zero-retention terms, an enterprise data-processing agreement) about what happens to it afterward, not a technical barrier before the call.
- Generic DLP
- Usually inspects traffic to detect or block a pattern; it does not rewrite the payload, so if the request is allowed through, the provider sees the raw content.
- DIY regex
- Only whatever the regex actually matches is removed before sending; anything the pattern misses reaches the provider raw.
Does the model still get useful context?
- zer0pii (masking gateway)
- Yes - placeholders preserve sentence structure and entity type, and are restored in the reply, so the model reasons over the same shape of text.Source: apps/gateway/core/reconstitutor.py
- Provider-side data controls
- Yes, always - since the model receives the unmodified text.
- Generic DLP
- Not applicable in the same sense - a block/alert tool does not rewrite the payload, so context is either fully present (allowed) or the request never proceeds (blocked).
- DIY regex
- Depends on the replacement chosen; a blunt find-and-delete regex can strip words the model needed, or leave gaps that read oddly.
Names, organizations, places (context-dependent entities)
- zer0pii (masking gateway)
- Semantic tier (NER) catches these when it fires; measured recall varies by entity and language, and is not perfect.Source: docs/DETECTION-EVAL.md (NER tier2: PERSON 76.2%, LOCATION 86.8%, ORGANIZATION 51.4% on the 1,500-row sample)
- Provider-side data controls
- Not addressed by this approach at all - it does not detect or redact anything, since it only governs what the provider does with whatever it receives.
- Generic DLP
- Weak on this class by design: DLP pattern libraries are built around structured identifiers (card numbers, ID formats), not free-text names or organizations, which do not have a fixed shape to match.
- DIY regex
- Effectively unable to catch these reliably - a name or place has no fixed pattern, so a regex either misses most of them or over-matches common words.
Checksum-valid IDs (cards, national IDs, IBANs)
- zer0pii (masking gateway)
- Deterministic tier validates structured identifiers against their checksum (Luhn, mod-97, etc.) before treating a match as real.Source: apps/gateway/core/deterministic.py; docs/DETECTION-EVAL.md (e.g. IBAN 99.92% recall against gold)
- Provider-side data controls
- Not addressed - this is a detection capability the provider-side control does not have.
- Generic DLP
- Often supported - checksum validation for common identifier formats is a standard DLP pattern-library feature.
- DIY regex
- Possible to hand-write, but each checksum algorithm is its own bug surface, and coverage is only as complete as the identifiers someone thought to add.
Streaming responses restored correctly?
- zer0pii (masking gateway)
- Yes - a dedicated reconstitutor handles token boundaries that split a placeholder across arbitrary SSE chunks, without buffering the full response.Source: apps/gateway/core/reconstitutor.py (StreamReconstitutor)
- Provider-side data controls
- Not applicable - nothing is masked, so there is nothing to restore in a stream.
- Generic DLP
- Not applicable to LLM response streams in the way it applies to network traffic; most DLP tooling targets file transfer and email, not token-by-token model output.
- DIY regex
- Hard to get right - a hand-rolled scan-and-replace over a stream has to handle a match split across two chunks, which is easy to miss and easy to get subtly wrong.
Code blocks left intact?
- zer0pii (masking gateway)
- Yes by design - markdown code blocks, variable names, package names and AST syntax are excluded from masking unless a hardcoded credential is confirmed.Source: CLAUDE.md (Core Principle 4, CODE RESILIENCE); apps/gateway/core/ast_filter.py
- Provider-side data controls
- Not applicable - nothing is rewritten, so code is never at risk of corruption from a redaction pass.
- Generic DLP
- Depends on the tool; a pattern scanner not written for source code can flag or mangle look-alike strings (a hex constant, a fake-looking test fixture) inside code.
- DIY regex
- A real risk - a general-purpose regex tuned for prose will often match inside code (a UUID literal, a phone-shaped test constant) and break it.
Proof of what happened (signed receipts)?
- zer0pii (masking gateway)
- Yes - each request is attested with an Ed25519 signature over a hash of the raw and sanitized payloads, verifiable independently of zer0pii.Source: apps/gateway/core/attestation.py (Attestor.sign, verify)
- Provider-side data controls
- Only whatever the provider's own logs or compliance reports state; no independently verifiable, per-request cryptographic proof is a standard feature of this approach.
- Generic DLP
- Usually an internal log entry in the DLP vendor's own console, not a portable, independently verifiable artifact.
- DIY regex
- None unless someone builds it - a homegrown script has no attestation layer unless it is added deliberately.
Latency cost
- zer0pii (masking gateway)
- Deterministic-only path: p50 under 10ms, p99 under 20ms. Semantic (NER) path: p50 under 300ms for a full pasted page, and it fails open to the deterministic path if it would exceed that budget.Source: CLAUDE.md (LATENCY BUDGET, PER PATH)
- Provider-side data controls
- None beyond the provider's own response time - there is no separate scanning step in front of the call.
- Generic DLP
- Varies by deployment (inline network inspection adds some latency; out-of-band monitoring adds none to the request itself), but is not a number this page can state without a specific product in mind.
- DIY regex
- Depends entirely on the implementation; a naive large regex set run per request can be slow, but there is no fixed number, since it is whatever code someone writes.
Setup effort
- zer0pii (masking gateway)
- Point the SDK base URL at the gateway host and add the API key - the request/response shape is unchanged otherwise.Source: apps/web (integrations/docs pages: base URL + header swap pattern)
- Provider-side data controls
- Usually a contract or account-tier change (enterprise terms, a data-processing addendum) rather than a code change.
- Generic DLP
- Typically an infrastructure deployment: an agent, a proxy, or a network tap, plus policy authoring - more operational surface than a client-side change.
- DIY regex
- Fast to start, since it is just a script - but every entity type, edge case and checksum has to be written and maintained by hand.
What it does NOT cover
- zer0pii (masking gateway)
- PASSWORD recall is 47.96% (fundamentally low-context strings) and PHONENUMBER recall is 84.41% on the gold eval; the semantic tier degrades to deterministic-only when it would exceed its latency budget; and Cursor and Claude Code's subscription-seat mode are not coverable (no base-URL override / no key to proxy for a subscription seat).Source: docs/DETECTION-EVAL.md (lines 64-65); CLAUDE.md (LATENCY BUDGET fail-open); docs: Developer tools (dev-tools.md)
- Provider-side data controls
- Does not detect or redact anything at all - it is a policy about the provider's handling of whatever it receives, not a technical control on the data itself.
- Generic DLP
- Free-text, context-dependent entities (names, organizations, addresses) with no fixed shape to match; also generally not designed for token-level LLM streaming output.
- DIY regex
- Anything the author did not anticipate - new formats, new locales, streaming, checksum validation, and code-safety are all separate problems a regex-only approach does not solve on its own.
Source: docs/DETECTION-EVAL.md, apps/gateway/core/pipeline.py, apps/gateway/core/reconstitutor.py, apps/gateway/core/deterministic.py, apps/gateway/core/attestation.py, apps/gateway/core/ast_filter.py, CLAUDE.md
When not to choose zer0pii
When not to choose zer0pii
If your only requirement is a contractual zero-retention promise from the model provider and you do not need entity-level detection, provider-side data controls alone may be simpler. If you need to inspect non-LLM traffic (email, file transfers, general network egress), that is DLP's actual scope, not this gateway's. And if your volume is tiny and your formats are fixed and known in advance, a small DIY regex may genuinely be enough - until the format changes.