Auditing de-identification runs
deidentify(..., audit=True) returns an AuditReport instead of the rewritten
text: a deterministic, PHI-free record of every redaction decision —
offsets, label, detector confidence and threshold, the action taken, content
hashes, model provenance, and projected residual risk. Sign it to make it
tamper-evident, retain it as compliance evidence, and verify it later without
ever touching the original text. Runs on-device.
When to use this skill
Use it whenever a de-identification needs to be defensible: regulatory
retention, internal review, reproducibility checks, or proving to an auditor
that identifiers were detected and handled — all without storing or exposing the
PHI itself.
Quick start
import openmed
note = "Patient John Doe (MRN 1234567) seen 2024-03-02. SSN 123-45-6789."
# Returns an AuditReport, NOT a DeidentificationResult, when audit=True.
report = openmed.deidentify(note, policy="hipaa_safe_harbor", audit=True)
# Make it tamper-evident with a release HMAC key (keep the key in a vault).
report.sign(b"my-release-hmac-key", key_id="release-2026")
# Persist the no-PHI report (no plaintext identifiers inside).
import json
with open("deid_audit.json", "w") as fh:
json.dump(report.to_dict(), fh, indent=2)
# Later: verify integrity. Optionally bind to the exact texts via their hashes.
ok = report.verify(b"my-release-hmac-key", original_text=note)
assert ok
What an AuditReport contains
AuditReport (from openmed.core.audit) carries no plaintext PHI. Key fields:
| Field |
Meaning |
policy |
policy profile name in effect |
resolved_profile |
the concrete settings applied (method, model, thresholds, language, sweep flags) |
detectors |
list[DetectorInfo] — provenance of each detector |
safety_sweep |
structured-ID sweep metadata (patterns version, source) |
spans |
list[AuditSpan] — one entry per detected identifier |
thresholds |
per-label confidence thresholds used |
residual_risk |
projected leakage / re-identification risk summary |
openmed_version |
library version that produced the report |
manifest_hash |
hash of the model manifest used |
document_length |
character length of the input |
input_hash |
sha256: hash of the original text |
deidentified_text_hash |
sha256: hash of the de-identified output |
repro_hash |
deterministic hash over the canonical payload |
signature |
AuditSignature once .sign() is called (else None) |
AuditSpan (per identifier, no plaintext): start, end, label,
canonical_label, sources, confidence, threshold, action, surrogate,
text_hash (hash of the span text), plus evidence and a redacted context.
DetectorInfo: source, model_id, model_format, commit, metadata.
AuditSignature: key_id, algorithm (HMAC-SHA256), value (hex digest).
Why audits store offsets + hashes + provenance, never plaintext
The whole point of de-identification is to not retain identifiers. An audit
log full of raw names and MRNs would itself be a PHI breach. So OpenMed records:
- offsets (
start/end) — where an identifier was, not what it was;
- hashes (
input_hash, deidentified_text_hash, per-span text_hash) —
prove what was processed without revealing it;
- provenance (
detectors, manifest_hash, openmed_version) — which
model/version made each call, for reproducibility;
- residual risk (
residual_risk) — projected leakage so a reviewer can
judge whether the output is safe to release.
This lets an auditor confirm coverage and integrity while the report stays
shareable.
Workflow
- Run with
audit=True and a policy= to get an AuditReport.
- Inspect
residual_risk before releasing the output — non-zero projected
leakage means review (cross-check with auditing-safe-harbor-checklist).
- Sign with
report.sign(key, key_id=...) using a secret release HMAC key
stored in a vault (never in the repo).
- Persist
report.to_dict() (JSON). For a reviewer view without full
context, use report.export_review_bundle().
- Verify on retrieval with
report.verify(key). Pass original_text= /
deidentified_text= to also confirm the texts match the recorded hashes.
- Retain per your retention schedule alongside the de-identified output
(but keep any reversible mapping separate and encrypted).
Hand-off to / from OpenMed
- Produce the de-id:
deidentifying-clinical-text (deidentify); add
audit=True to get the report from the same call.
- Coverage check:
auditing-safe-harbor-checklist maps span labels to the
18 HIPAA identifier categories and flags gaps.
- Policies:
configuring-privacy-policies — resolved_profile reflects the
chosen policy=.
- Other surfaces: MCP
openmed_deidentify and REST POST /pii/deidentify
drive the same engine; audit output is available through them.
Edge cases & gotchas
audit=True changes the return type to AuditReport. You get the report,
not .deidentified_text. Run a separate normal call if you also need the text.
- An unsigned report is not tamper-evident.
.sign() is a deliberate second
step; signature is None until you call it. Empty/None keys are rejected.
verify needs the same key. It checks the HMAC and the repro_hash;
optionally it re-hashes texts you pass to confirm they are the audited ones.
- Never put plaintext PHI back into the report. Do not stuff raw identifiers
into
metadata/evidence; the design is hash-and-offset only.
- Store the signing key in a secret manager, not in source or the report.
- Residual risk is advisory, not a pass/verdict. Combine it with the Safe
Harbor checklist and human review for release decisions.
Standards & references
1---2name: auditing-deidentification-runs3description: Produce a signed, reproducible, no-PHI audit trail for an OpenMed de-identification run via deidentify(audit=True). Use when the user needs compliance evidence, a tamper-evident record of what was redacted and why, to verify nothing was changed, to retain proof for HIPAA/GDPR audits, or to review de-id decisions without exposing plaintext PHI. Covers the AuditReport / AuditSignature / AuditSpan / DetectorInfo fields, why audits store offsets+hashes+provenance+residual-risk and never plaintext, signing with .sign(key), and verifying with .verify(key). Pairs with OpenMed deidentifying-clinical-text and auditing-safe-harbor-checklist.4license: Apache-2.05---67# Auditing de-identification runs89`deidentify(..., audit=True)` returns an `AuditReport` instead of the rewritten10text: a deterministic, **PHI-free** record of every redaction decision —11offsets, label, detector confidence and threshold, the action taken, content12hashes, model provenance, and projected residual risk. Sign it to make it13tamper-evident, retain it as compliance evidence, and verify it later without14ever touching the original text. Runs **on-device**.1516## When to use this skill1718Use it whenever a de-identification needs to be *defensible*: regulatory19retention, internal review, reproducibility checks, or proving to an auditor20that identifiers were detected and handled — all without storing or exposing the21PHI itself.2223## Quick start2425```python26import openmed2728note = "Patient John Doe (MRN 1234567) seen 2024-03-02. SSN 123-45-6789."2930# Returns an AuditReport, NOT a DeidentificationResult, when audit=True.31report = openmed.deidentify(note, policy="hipaa_safe_harbor", audit=True)3233# Make it tamper-evident with a release HMAC key (keep the key in a vault).34report.sign(b"my-release-hmac-key", key_id="release-2026")3536# Persist the no-PHI report (no plaintext identifiers inside).37import json38with open("deid_audit.json", "w") as fh:39 json.dump(report.to_dict(), fh, indent=2)4041# Later: verify integrity. Optionally bind to the exact texts via their hashes.42ok = report.verify(b"my-release-hmac-key", original_text=note)43assert ok44```4546## What an AuditReport contains4748`AuditReport` (from `openmed.core.audit`) carries no plaintext PHI. Key fields:4950| Field | Meaning |51| --- | --- |52| `policy` | policy profile name in effect |53| `resolved_profile` | the concrete settings applied (method, model, thresholds, language, sweep flags) |54| `detectors` | `list[DetectorInfo]` — provenance of each detector |55| `safety_sweep` | structured-ID sweep metadata (patterns version, source) |56| `spans` | `list[AuditSpan]` — one entry per detected identifier |57| `thresholds` | per-label confidence thresholds used |58| `residual_risk` | projected leakage / re-identification risk summary |59| `openmed_version` | library version that produced the report |60| `manifest_hash` | hash of the model manifest used |61| `document_length` | character length of the input |62| `input_hash` | `sha256:` hash of the original text |63| `deidentified_text_hash` | `sha256:` hash of the de-identified output |64| `repro_hash` | deterministic hash over the canonical payload |65| `signature` | `AuditSignature` once `.sign()` is called (else `None`) |6667`AuditSpan` (per identifier, no plaintext): `start`, `end`, `label`,68`canonical_label`, `sources`, `confidence`, `threshold`, `action`, `surrogate`,69`text_hash` (hash of the span text), plus `evidence` and a redacted `context`.7071`DetectorInfo`: `source`, `model_id`, `model_format`, `commit`, `metadata`.7273`AuditSignature`: `key_id`, `algorithm` (HMAC-SHA256), `value` (hex digest).7475## Why audits store offsets + hashes + provenance, never plaintext7677The whole point of de-identification is to *not* retain identifiers. An audit78log full of raw names and MRNs would itself be a PHI breach. So OpenMed records:7980- **offsets** (`start`/`end`) — *where* an identifier was, not what it was;81- **hashes** (`input_hash`, `deidentified_text_hash`, per-span `text_hash`) —82 prove what was processed without revealing it;83- **provenance** (`detectors`, `manifest_hash`, `openmed_version`) — *which*84 model/version made each call, for reproducibility;85- **residual risk** (`residual_risk`) — projected leakage so a reviewer can86 judge whether the output is safe to release.8788This lets an auditor confirm coverage and integrity while the report stays89shareable.9091## Workflow92931. **Run with `audit=True`** and a `policy=` to get an `AuditReport`.942. **Inspect `residual_risk`** before releasing the output — non-zero projected95 leakage means review (cross-check with `auditing-safe-harbor-checklist`).963. **Sign** with `report.sign(key, key_id=...)` using a secret release HMAC key97 stored in a vault (never in the repo).984. **Persist** `report.to_dict()` (JSON). For a reviewer view without full99 context, use `report.export_review_bundle()`.1005. **Verify on retrieval** with `report.verify(key)`. Pass `original_text=` /101 `deidentified_text=` to also confirm the texts match the recorded hashes.1026. **Retain** per your retention schedule alongside the de-identified output103 (but keep any reversible mapping separate and encrypted).104105## Hand-off to / from OpenMed106107- **Produce the de-id:** `deidentifying-clinical-text` (`deidentify`); add108 `audit=True` to get the report from the same call.109- **Coverage check:** `auditing-safe-harbor-checklist` maps span labels to the110 18 HIPAA identifier categories and flags gaps.111- **Policies:** `configuring-privacy-policies` — `resolved_profile` reflects the112 chosen `policy=`.113- **Other surfaces:** MCP `openmed_deidentify` and REST `POST /pii/deidentify`114 drive the same engine; audit output is available through them.115116## Edge cases & gotchas117118- **`audit=True` changes the return type** to `AuditReport`. You get the report,119 not `.deidentified_text`. Run a separate normal call if you also need the text.120- **An unsigned report is not tamper-evident.** `.sign()` is a deliberate second121 step; `signature` is `None` until you call it. Empty/None keys are rejected.122- **`verify` needs the same key.** It checks the HMAC and the `repro_hash`;123 optionally it re-hashes texts you pass to confirm they are the audited ones.124- **Never put plaintext PHI back into the report.** Do not stuff raw identifiers125 into `metadata`/`evidence`; the design is hash-and-offset only.126- **Store the signing key in a secret manager**, not in source or the report.127- **Residual risk is advisory, not a pass/verdict.** Combine it with the Safe128 Harbor checklist and human review for release decisions.129130## Standards & references131132- HIPAA de-identification & documentation, 45 CFR 164.514(b): https://www.hhs.gov/hipaa/for-professionals/privacy/special-topics/de-identification/index.html133- HMAC-SHA256: RFC 2104 / FIPS 198-1.134- OpenMed source: `openmed/core/audit.py` (`AuditReport`, `AuditSpan`,135 `AuditSignature`, `DetectorInfo`, `sign`, `verify`), `openmed/core/pii.py`136 (`deidentify(audit=True)`).