Subproject · Tool
er7-redact
Remove patient detail from a message — without moving its shape, so everything downstream still behaves the way it did on the original.
What it is
A bug report arrives with a real message attached, and somebody has to turn it into something
that can be committed to a repository, pasted into a ticket, or fed to a test suite. er7-redact does that: a policy of HL7 paths and actions, applied
to a message, plus a report of every position it changed.
The design constraint is that the redacted message has to survive everything that was going to be done to the original. So redaction rewrites leaf text only: the same segments, fields, repetitions, components, and subcomponents come back out, and every path that resolved to a value still resolves to one.
PID|1||444333222^^^ACME&1.2.840.114398.1.100&ISO^MR||EVERYWOMAN^EVE^E||19620320|FPID|1||73f419a72093eda0^^^ACME&1.2.840.114398.1.100&ISO^MR||REDACTED^REDACTED^REDACTED||1962|FThe record number is a stable pseudonym, the name is a placeholder, the birth date is a year — and the assigning authority, the identifier type, and the sex are untouched, because none of them identifies the patient.
Install
cargo add er7-redactcargo install er7-redactTutorial From a message you cannot share to one you can
1. Redact with the built-in policy
er7-redact message.er7 > redacted.er7 Or, in Rust:
use er7_redact::{Policy, Redactor};
let mut message = er7::parse(text)?;
let report = Redactor::new(Policy::patient_identifiers()).redact(&mut message);
println!("{}", message.to_er7()); The built-in policy names about forty positions across PID, NK1, PV1, GT1, and IN1 — the fields that carry a patient
identifier in every HL7 v2 release from 2.3 to 2.9.
2. Check what it did, before sharing anything
er7-redact --report message.er7 PID[1]-3[1].1.1 pseudonym
PID[1]-5[1].1.1 replace REDACTED
PID[1]-5[1].2.1 replace REDACTED
PID[1]-5[1].3.1 replace REDACTED
PID[1]-7[1].1.1 first 4 One row per position that actually changed, each a fully qualified path you can paste into er7 --query to see what is there now. A report carries no values — not the old text, not the new — so it is safe to paste into a ticket. An empty report means
nothing was found to redact, which is either good news or a wrong policy.
3. Say what you want redacted
er7-redact -r "PID-5 replace REDACTED" -r "PID-7 first 4" message.er7 Naming your own rules replaces the built-in policy rather than adding to it, so what runs can be predicted from the arguments. In Rust the same policy reads:
use er7_redact::{Action, Policy};
let policy = Policy::new()
.with("PID-5", Action::redacted())?
.with("PID-7", Action::First(4))?; Paths are er7 paths, so everything that notation does works
here: OBX-5 covers every OBX in the message, PID-13[2] names one repetition, PID-5.1 reaches one component.
4. Keep the built-in list, and pin it
er7-redact --show-policy > de-identify.policy # the built-in list, as a file
$EDITOR de-identify.policy
er7-redact --policy de-identify.policy message.er7 The built-in list may grow in a minor release — if a position turns out to carry patient detail, waiting for a major version to start redacting it would be the wrong trade — so a repository that needs the same redaction next year should check the file in.
5. When you do not trust the list
er7-redact --all message.er7 Redact everything except the MSH header, then name what to keep. This is the only
thing that covers a local Z segment nobody has documented, and the only honest
answer to “is there anything else in there?”.
let policy = Policy::everything()
.with("OBX-2", Action::Keep)?
.with("OBX-3", Action::Keep)?
.with("OBX-5", Action::Keep)?;Help Reference
The eight actions
| Written | PATID1234 becomes | Use for |
|---|---|---|
keep | PATID1234 | exempting a position from a fallback |
clear | | an address, a phone number — anything a placeholder would not help |
null | "" | telling a receiver to clear its stored value |
replace REDACTED | REDACTED | a name, where a placeholder reads better than a blank |
mask * | ********* | a value whose length is wanted and whose content is not |
first 4 | PATI | a birth date reduced to its year |
last 4 | 1234 | an account number reduced to the digits a human matches on |
pseudonym | 73f419a72093eda0 | an identifier that must stay linkable across messages |
clear and null are not interchangeable. An empty field says
“the sender said nothing about this”; "" says “delete your
stored value”. A redacted copy for testing should not be telling anything to delete a
record.
The policy file
# de-identify.policy
PID-3.1 pseudonym # keep linkage, lose the record number
PID-5 replace REDACTED
PID-7 first 4 # a birth date reduced to its year
PID-11 clear
NTE-3 clear # free text, where identifiers hide
* replace REDACTED # optional: the fallback, always applied last One rule per line: a path, whitespace, an action. Blank lines are ignored, # starts a comment, and rules apply in order. A path of * sets the
fallback, which turns the policy from “redact these” into “redact everything
except these” — and keep is how a position is exempted from it.
A malformed line is refused at load time, with the line number. That strictness is deliberate: a typo in a policy means a value that silently was not redacted.
Command-line options
| Option | Effect |
|---|---|
-p, --policy <FILE> | Read rules from a policy file; may be repeated |
-r, --rule <RULE> | Add one rule, e.g. "PID-5 replace REDACTED"; may be repeated |
-a, --all | Redact everything except the MSH header |
-k, --key <KEY> | Pseudonym key, a number; default 0 |
-m, --message <N> | Use only the Nth message of the input |
-t, --terminator <KIND> | Segment terminator to write: cr (default), lf, crlf |
-o, --output <FILE> | Write to FILE instead of standard output |
--report | Write what would change, and not the message |
--show-policy | Write the policy that would be applied, and exit |
Pseudonyms
Clearing an identifier destroys the message as test data: nothing ties the patient here to the
same patient in the next message. pseudonym replaces it with a stable stand-in
instead, so every message redacted with the same key maps PATID1234 the same way, and different keys produce unrelated mappings.
A pseudonym is not a cryptographic guarantee. It is a keyed hash that preserves equality on purpose — that is what makes messages joinable, and it is also what leaks. Anyone holding the key can invert the mapping by trying every candidate identifier, and record numbers come from small spaces.
Use it inside your own trust boundary — test environments, reproductions, CI fixtures. For
data leaving it, use clear or replace, which leak nothing but the
fact that a value was there.
Examples
Six runnable programs in the crate's examples/ directory. Each asserts its own results, so a clean exit means it passed.
| Example | Shows |
|---|---|
redact_a_message | The built-in policy end to end: what goes, what stays, and why the message still works. |
write_a_policy | Three ways to say what to redact — in Rust, from a file, or by extending a built-in. |
redact_all_but | A fallback over everything, with keep rules for what a test needs. |
pseudonyms_and_linkage | Why an identifier becomes a pseudonym rather than a blank, and what that costs. |
read_the_report | The audit trail: one row per position changed, and no values in it. |
redact_absent_empty_null | The three states HL7 keeps apart, and why redaction leaves two of them alone. |
cargo run --example redact_a_messageWhat it does not do
This is a positional editor, not a compliance tool.
- It cannot tell you whether the result is de-identified. That is a judgement about a whole data set, its recipients, and what else they hold — made by a person who is accountable for it.
- It does not know which positions your senders use. Run
er7 message.er7and read what is actually in there before trusting any policy, including the built-in one. - It does not find an identifier written into free text. A name in an
NTE-3comment survives every positional policy; name that position, or use--all. - There is no way back: no mapping table, no key escrow, no undo.
A message this crate has redacted is a message with less in it, which is progress, and is not the same thing as a safe one.