A coding agent that reads one payroll export ingests FEINs, SSNs, and bank account numbers straight into a cloud model's context. Once those bytes are in the request, they're gone β no recall, no clawback. ClaimCheck is our answer: a free, MIT-licensed, fully local sanitizer that rewrites PII into stable placeholder tickets before a model ever sees them, keeps the mapping in a vault on your disk, and restores the real values when the work is done. Source and a Windows binary live at github.com/saasclawai-org/ClaimCheck.
This is the build story: what it does, how it's shaped, what leaked first, and the numbers.
The failure mode is the client, not the API
September 2026 supplied the case study. Zhipu's ZCode coding client was caught compressing user code pending upload to third-party cloud storage without consent. Remediation came fast β data deleted, deletion third-party verified, the client open-sourced, a no-retention policy added to the platform. But the lesson is structural: your agreement is with the API provider. The client is a separate trust surface, and it can leak on its own schedule.
We run Cline against a cloud GLM endpoint every day, and our repos are full of exactly the data you don't want in a prompt β employer ID numbers, account numbers, credentials in config files. Waiting on a vendor's data-processing agreement to cover what the client does is not a control. Scrubbing on our side of the wire, before anything leaves the machine, is.
Claim tickets, not redaction
Naive redaction destroys the thing an LLM needs most: referential integrity. Replace every identifier with *** and the model can't tell your two companies apart, so it can't reason about the data at all. ClaimCheck mints stable, per-project placeholders instead: [FEIN_1] always means the same FEIN within a project, [PERSON_2] the same person.
The end-to-end proof case:
Input: Jane Doe from Microsoft visited Seattle to meet Satya Nadellaβ¦
Output: [PERSON_1] from [ORG_1] visited [LOC_1] to meet [PERSON_2]β¦
Zero plaintext in tool output, and the sentence still parses β the model can reason about who visited whom without knowing who anyone is. The mapping lives in a local JSON vault (0700 directory, 0600 files), and a restore CLI round-trips a sanitized file back to real values after the LLM stage is finished. Vault writes are atomic (temp file + rename, so a crash can't tear one), and minting is covered by a 24-thread concurrency hammer.
Three ways to run it
- MCP server over stdio β
read_sanitized(path),scan_file(path),sanitize_text(text). Drop it into any MCP client and agents read files through the sanitizer instead of around it. - Streamable HTTP β the same tools hosted on a local port, path-jailed with
SANITIZE_ROOTS, reachable over an SSH tunnel or a private endpoint. - Sanitizing proxy β the interesting one. A Kestrel proxy sits between your coding tool and the model provider, rewrites PII out of every request body β including streaming SSE, where placeholders split across chunks get reassembled before restoring β and restores real values in the response. This is the model hop: even a leaky client can't ship raw data upstream, because it never touches a raw byte.
The proxy is fail-closed by default: if scrubbing fails, the request fails, not the privacy.
22 recognizers, plus an opt-in NER tier
The regex tier covers FEIN, SSN, email, phone, Luhn-validated card numbers, IBAN, AWS/GitHub/Slack and generic API keys, JWTs, PEM private-key blocks, Password= connection strings, plus keyword-anchored CVV, expiry, DOB, passport, driver-license, routing-number, IP, insurance, medical-license, and bank-account patterns β 22 recognizer types as of v0.2.0. Encoded blobs (base64/hex) are decoded and masked before they can smuggle anything past.
The NER tier catches what regex can't: names, organizations, locations. It's a quantized BERT token-classification model (~109 MB ONNX) that runs locally, gated behind SANITIZE_NER=1 because a 109 MB surprise download is rude. Cost: roughly 1β3 seconds per sanitize call. It reliably turns "Jane Doe from Microsoft visited Seattle" into PERSON/ORG/LOC tickets with zero false positives in our tests β and large requests skip it automatically above a 200K-token guard, falling back to the regex tier.
We probed our own build. It leaked.
Before shipping v0.1.1 we ran a gap probe against our own released binary. Three complete leaks came back:
- JWTs β dots and underscores defeated the encoded-blob scanner, and every segment sat under the length heuristic. The highest-frequency real-world credential leaked completely.
- Connection strings β
Password=β¦had no recognizer at all. - PEM private keys β a key's base64 body decodes to binary DER, so decode-and-rescan found nothing to match. Structurally, decode-and-rescan cannot catch binary-encoded secrets.
All three shipped as recognizers the same day (JWT shape, PEM BEGIN/END block matching, password lookbehind), and the recognizer test suite grew to 12 types at that point. It's 22 now.
The second round came from the field: a Raspberry Pi deployment's smoke test found sk-live-style keys missed because the pattern demanded 20+ characters after the prefix, and bare bank account numbers with nothing covering them. Fixed, suite green, released. Field smoke tests on real machines keep catching what synthetic suites miss β send builds to weird hardware early.
Three bugs worth stealing lessons from
The stale artifact. v0.1.0 briefly shipped a pre-NER executable β the feature commit landed after the build and nobody re-published. We caught it because a Windows install kept behaving regex-only; a UTF-16 grep of the binary showed the SANITIZE_NER string simply wasn't there. Rule now: after feature commits, rebuild and grep the artifact for the feature's literal strings before packaging.
[PERSON_1]e. WordPiece splits names into subword pieces ("Nadella" β "Na" + "##dell" + "##a"), and B-labeled fragments spawned adjacent entity spans. Naive snapping made spans overlap and produced corrupted output like [PERSON_6]della. Fix: snap entity spans to full word boundaries, then merge same-type touching spans β with a regression invariant in the suite that no placeholder is ever directly followed by a letter or digit.
Push protection. GitHub blocked a push because credential-shaped test fixtures looked real. Fix: assemble those fixtures at runtime by string concatenation β the scanner sees no literal, the test still feeds real shapes.
Numbers from the wire
We pushed a 4 MB (~1M-token) body through the proxy: 0.69s end-to-end vs 0.16s direct β +0.53 seconds of scrub overhead per million tokens, with the regex tier covering the full body and the NER auto-skip proving itself on the wire. Kestrel's 30 MB body cap leaves roughly 7M tokens of headroom. Scrub latency is not the bottleneck; the model is.
OpenClaw is the first production consumer: the proxy runs in front of our own gateway's model traffic, NER live, every session scrubbed by default β including this one.
Local-first, on purpose
We considered hosting ClaimCheck as a service and said no. A hosted sanitizer is a PII processor, with the SOC 2/HIPAA/BAA liability that implies β and it would contradict the pitch. The MIT core stays local and free; the roadmap (OCR pass, DPAPI/Keychain vault encryption, pluggable recognizer packs, audit logging) forms the eventual paid tier via open-core licensing.
Validation arrived from an odd direction: an identical-concept tool shipped the same week we named ours. Two teams independently building the same thing in the same week is the market telling you the gap is real.
If you're running an AI coding tool against cloud models, the question isn't whether your repos contain PII β it's whether the tool is allowed to see it. Check your PII at the door: hand the model claim tickets instead. More build stories on the SaaSClaw blog; self-hosting and toolkit docs at /docs.