Adversarially scan already-de-identified clinical text for residual identifiers and emit a leakage report that blocks release on any hit. Use after OpenMed de-identification when the user asks to verify a redaction, prove no PHI/PII leaked, gate a dataset before sharing, or run a second-pass detector. Covers format and checksum detectors (SSN, Luhn for card numbers, MRN/account patterns, emails, phones, dates), entropy heuristics for high-randomness tokens, severity scoring, and a hard block-on-leak rule. This is the verification half of OpenMed's leakage-first ethos. Hand-off: re-run openmed.extract_pii on the de-id output and diff against expectations. License-free, local-first. Pairs after deidentifying-clinical-text.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
De-identification is verified, not assumed. A model-driven redaction can miss a structured identifier (an SSN typo'd with spaces, an account number in a footer, a date in an odd format) — and a single residual identifier defeats the whole release. This skill is the adversarial second pass: scan the output of de-identification for anything that still looks like an identifier, score it, and block release on any leak. It is the verification half of OpenMed's leakage-first ethos — gate on leakage, not on F1.
deidentifying-clinical-text, before the de-identified text leaves
a trust boundary (export, share, train, publish).Run this on the de-identified text, not the original. The original is expected to be full of identifiers.
Two complementary passes — a deterministic structural scan plus a model second-pass diff:
import re
import openmed
# Synthetic — the de-identified OUTPUT we are auditing for residual leaks.
deid_text = "Patient [NAME] seen on [DATE]. Backup contact 415-555-0184; acct 4111111111111111."
def luhn_ok(digits: str) -> bool:
nums = [int(d) for d in digits]
nums[-2::-2] = [(2 * d - 9 if 2 * d > 9 else 2 * d) for d in nums[-2::-2]]
return sum(nums) % 10 == 0
DETECTORS = {
"SSN": (r"\b\d{3}-\d{2}-\d{4}\b", "critical", None),
"EMAIL": (r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b", "high", None),
"PHONE": (r"\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b", "high", None),
"DATE": (r"\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b", "medium", None),
"MRN": (r"\bMRN[:#\s]*\d{5,}\b", "high", None),
"CARD": (r"\b(?:\d[ -]?){13,19}\b", "critical", luhn_ok), # checksum-gated
}
findings = []
for label, (pattern, severity, checksum) in DETECTORS.items():
for m in re.finditer(pattern, deid_text, flags=re.IGNORECASE):
token = m.group()
if checksum and not checksum(re.sub(r"\D", "", token)):
continue # fails Luhn -> not a real card number, skip
findings.append({"label": label, "severity": severity,
"start": m.start(), "end": m.end()}) # offsets, not text
# Second-pass model detector: re-run PII extraction on the de-id output.
residual = openmed.extract_pii(deid_text) # PredictionResult
for ent in residual.entities:
findings.append({"label": ent.label, "severity": "high",
"start": ent.start, "end": ent.end})
leaked = bool(findings)
print({"leak": leaked, "count": len(findings)}) # report carries NO plaintext
assert not leaked, "Release BLOCKED: residual identifiers detected."Note what the report records: labels, severities, and offsets — never the leaked plaintext. Echoing the leaked identifier into a report or log re-creates the exact PHI exposure you are auditing for.
openmed.extract_pii on the output and
treat any returned entity as a residual leak. Because it's a different
detector than the one that did the redaction, it catches different misses.findings is
non-empty at high/critical, fail the export. Surface a no-PHI report
(counts + offsets + severities) so a reviewer can locate and re-redact.deidentifying-clinical-text: this skill consumes
result.deidentified_text. Never audit result.original_text.from openmed import extract_pii — re-run it
on the de-id output and diff. Equivalent MCP/REST surfaces detect PII spans for
the same purpose. Any span returned on already-de-identified text is a leak.reviewing-reidentification-risk: zero direct-identifier leaks is
necessary but not sufficient — quasi-identifiers (age + ZIP + date) can still
re-identify. Hand a clean-on-leakage dataset to QI risk scoring next.evaluating-with-leakage-gates: wire this scan into the eval harness so
a leakage regression fails CI, not just an F1 drop.method="replace", the output
contains fake names/emails by design. The model second-pass may flag them —
diff against the known mapping/surrogate set so you don't block on synthetic
data. True leaks are values present in the original text.dd/mm/yyyy, yyyy.mm.dd, NHS/SIN/fiscal-code
formats vary; tune detectors to the data's locale or you under-detect.maziyarpanahi/openmed
00e9efd
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.