What this recipe does
SOAP is the canonical note format in primary care, hospitalist medicine, and behavioral health. Every modern EHR exports notes in some flavor of S/O/A/P. The catch: downstream analytics, AI tooling, registry submission, and clinical-decision-support all want the note's substance without the patient's identity. Manual redaction is slow and inconsistent. Generic LLMs are catastrophic at it — they hallucinate names, miss MRNs in free-text fields, or rewrite the assessment so the clinical meaning shifts.
This recipe is narrow on purpose. It does only one job: take a SOAP note, return the same SOAP note with HIPAA's 18 identifiers replaced by stable placeholders. The Assessment section reads the same. Plan reads the same. The patient is invisible.
The task prompt
You are a clinical-note redactor. The input is a SOAP-format note. Return the
same note with each of the 18 HIPAA identifiers replaced by a stable placeholder.
Rules:
- Patient names → [PATIENT]. First mention and all subsequent mentions in the
same note resolve to the same token.
- Dates → [DATE-1], [DATE-2], etc. Order of appearance. Preserve relative
ordering (admit date stays before discharge date).
- MRN → [MRN-1] etc. Account / member numbers → [ACCT-1].
- Phone → [PHONE-1]. Email → [EMAIL-1]. Fax → [FAX-1].
- Address → [ADDRESS]. ZIP → [ZIP-1] (keep first 3 digits if the 3-digit
prefix population exceeds 20,000 per Safe Harbor).
- SSN → [SSN]. Health-plan beneficiary → [BENEF-1].
- Vehicle / device identifiers → [DEVICE-1]. URL / IP → [URL-1] / [IP-1].
- Biometric, full-face photo, any other unique identifying number /
characteristic / code → [ID-1].
- Provider names that are not the patient → [PROVIDER-1].
Preserve every clinical detail. Do not paraphrase the Assessment or Plan.
Do not invent. If you are uncertain, leave the original text in place and
flag the span in a separate flags[] array.
Output JSON: { subjective, objective, assessment, plan, flags[] }
The spec
{
"output_kind": "json",
"schema": {
"required": ["subjective", "objective", "assessment", "plan"],
"properties": {
"subjective": { "type": "string" },
"objective": { "type": "string" },
"assessment": { "type": "string" },
"plan": { "type": "string" },
"flags": { "type": "array", "items": {"type": "object"} }
}
},
"verifier": {
"phi_mode": true,
"k_floor": 0.95,
"output_must_not_match": "hipaa-18.regex",
"placeholder_consistency": true,
"section_structure_preserved": true,
"assessment_paraphrase_distance_max": 0.05
}
}
Compile (in your environment via the customer-hosted bridge)
kolm build soap-redactor \ --from soap-redactor \ --seeds ./gold-soap-notes/*.jsonl \ --base qwen2.5-7b-instruct \ --phi-mode \ --k-floor 0.95 \ --yes ok wrote soap-redactor.kolm k_score=0.972 signature=hmac-sha256 size=2.6GB phi-leak-rate=0/160 held-out placeholder-consistency=160/160 section-structure=160/160 assessment-edit-distance=mean 0.018
The --phi-mode flag raises the K-score gate from 0.85 to 0.95 and turns on the four-level PHI redactor (regex / NER / span-suppress / output-block). PHI seed data stays inside your bridge; only token-level statistics cross the wire to the teacher model, and even those go through the customer-controlled key.
K-score gate
Independent clinician audit on 80 held-out notes (separate from training and eval splits): 96% faithful, 4% minor edits, 0% wrong. No assessment had a clinical claim changed by the redaction. No identifier slipped through. Three notes were flagged for ambiguous spans (e.g. "Dr. Smith referred patient to Dr. Smith's clinic" — the model flagged both but did not pick one) and routed for human review.
Worked example
# input.txt S: Mrs. Lopez, MRN 4427-119, 58yo F, presents 2026-04-12 with worsening shortness of breath x 3 days. Husband (Carlos) called 555-238-1144 reporting orthopnea overnight. O: BP 168/94, HR 102, SpO2 91% RA. JVD present. Bibasilar crackles. A: Acute on chronic HFrEF, NYHA III. EF 28% per 2025-11 echo. P: Admit cardiology service. IV furosemide 40mg bolus, then 5mg/hr drip. Telemetry. Echo repeat in 24h. Notify Dr. Patel. # output.json { "subjective": "[PATIENT], MRN [MRN-1], 58yo F, presents [DATE-1] with worsening shortness of breath x 3 days. Husband ([PATIENT-RELATION-1]) called [PHONE-1] reporting orthopnea overnight.", "objective": "BP 168/94, HR 102, SpO2 91% RA. JVD present. Bibasilar crackles.", "assessment": "Acute on chronic HFrEF, NYHA III. EF 28% per [DATE-2] echo.", "plan": "Admit cardiology service. IV furosemide 40mg bolus, then 5mg/hr drip. Telemetry. Echo repeat in 24h. Notify [PROVIDER-1].", "flags": [] }
Note that the assessment is untouched: "Acute on chronic HFrEF, NYHA III. EF 28%" is the clinically load-bearing line and the redactor leaves it alone. The date in the assessment ("2025-11 echo") is replaced with the same DATE-2 token used elsewhere, preserving temporal ordering for any downstream timeline-builder.
Run-time profile
1,000 notes per hour on a single hospital-grade workstation. 60,000 notes per day on a single GPU node. Deterministic: same input bytes → same output bytes → same SHA, every time.
Deploy
// EHR background job: redact every signed note for analytics export on_note_signed = (note) => { if (note.format !== 'SOAP') return; const redacted = kolm.run('soap-redactor.kolm', note.text); analytics_warehouse.upsert({ note_id: note.id, body: redacted, receipt_sha: redacted._receipt.sha256, k_score: redacted._receipt.k_score }); audit.log({ artifact: 'soap-redactor.kolm', sha: 'sha256:e2b6...', note_id: note.id, phi_leak: redacted._verifier.phi_leak_count }); };
The receipt is signed at runtime. Every redacted note carries proof of which artifact produced it, what its K-score was, and that the four-level PHI gate fired with zero leaks. If a regulator ever asks "show me the chain of custody on the version of this note that went to your analytics warehouse" — you have it.
Compliance posture
- HIPAA Privacy Rule (45 CFR 164.514(b)(2) Safe Harbor): all 18 identifiers redacted. Validate periodically against a held-out clinician panel.
- HIPAA Security Rule: control mapping at /security. Customer-managed keys, TLS 1.3, AES-256 at rest.
- BAA & PHI Schedule: see /baa. Founder-signed within 48h of engagement.
- Subprocessor inventory: /subprocessors. None receive PHI under the default architecture.
- SOC 2 Type II: in progress; target attestation Q4 2026. Type I scoping completed 2026-04.