← Yoonbo Cho

What I learned de-identifying clinical documents for an LLM

Oct 26, 2026

Notes from building Arbiter, a clinical-document RAG prototype that runs on synthetic records only.

The rule I started with was simple: nothing reaches a model until the document has been de-identified. Every other decision followed from that.

I want to be upfront about what Arbiter is. It's a prototype, launched in September 2026, tested on 20 synthetic documents and 40 gold questions. No real patients, no customers, no compliance claims. But building it taught me where de-identification goes wrong, and most of it applies to any team putting health data near an LLM.

HIPAA gives you two routes

The Safe Harbor method removes 18 kinds of identifiers: names, most of a ZIP code, all date elements except the year, phone numbers, medical record numbers and so on. The other route, Expert Determination, has a qualified statistician certify that re-identification risk is very small. A prototype like mine works toward Safe Harbor because you can test it mechanically.

Make "de-identified" a state the code checks

In Arbiter, a document moves through a pipeline (FastAPI, Redis Streams, LangGraph, Postgres with pgvector), and the model step refuses to run unless the document is marked de-identified. Presidio scrubs it before indexing. The map back to the original identifiers is encrypted per tenant. Nobody has to remember the rule, because the pipeline enforces it.

Your recall number can lie

My first metric matched whole entities and read 0.967. Then I scored each address component separately. ZIP codes had survived in 14 of 20 documents and street lines in 7. Under Safe Harbor, you may keep only the first three digits of a ZIP, and only where that area has more than 20,000 people. A whole-string metric hid exactly the identifier that matters. With a strict per-component metric and new recognizers, it reached 1.000.

Default recognizers don't know charts

Presidio is a good base, but on clinical text:

That last one is the over-redaction problem nobody warns you about. Strip too much and the model can't answer the clinical question, so people start looking for ways around the filter.

Check the output, too

An answer is rejected if it leaks an identifier, misses a citation, or scores below 0.7 on faithfulness. The tests found a bug the eval didn't: the output leak check used a looser threshold than the input scrub, so a bare SSN redacted on the way in could pass on the way out. Use one standard in both directions.

Isolation has to be real

Row-level security sits on every table holding PHI, and retrieval is hard-filtered by patient. During bring-up I found the services connecting as the Postgres superuser, which bypasses row-level security entirely. The policies were there and none of them applied. Cross-tenant probes now prove it: another tenant's job returns 404, zero rows are visible, and cross-tenant inserts are rejected.

Contracts come before processing

Any vendor that handles PHI for you needs a Business Associate Agreement. Arbiter has a gate for that: a tenant can't process anything until its BAA is marked signed.

Takeaways

  1. Enforce de-identification as a pipeline state, not a convention.
  2. Score each identifier component separately. ZIP codes and dates are where whole-string metrics fool you.
  3. Test recognizers on realistic documents, and watch for over-redaction as closely as for leaks.
  4. Run the same leak check on the way out as on the way in.
  5. Prove tenant isolation with probes. Make sure the app connects as a role that row-level security actually binds.

I'm looking for an AI engineering role (remote, US). If your team puts sensitive data near LLMs, I'd love to talk.