Choose a policy and read the manifest
Every pass runs under one policy: a transform for each of the 18 Safe Harbor categories. This guide
picks one, derives a site policy from it, and reads the manifest the pass returns. The same options
work for the core deidentify and for the HL7 v2, C-CDA, FHIR, X12 and NCPDP adapters; the DICOM
adapter takes the policy and its own UID options.
Use the built-in default
safe-harbor is the built-in default: pass no policy and you get it. Under it a name or an
identifier is removed, a date keeps its year, a ZIP keeps a safe three-digit prefix (or 000), and the (R)
catch-all is blocked. It uses no keyed transform, so it needs no key.
import { deidentify, SAFE_HARBOR_POLICY, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
SAFE_HARBOR_POLICY.name; // => "safe-harbor"
SAFE_HARBOR_POLICY.transforms[SAFE_HARBOR_CATEGORIES.MRN]; // => "redact"
SAFE_HARBOR_POLICY.transforms[SAFE_HARBOR_CATEGORIES.OTHER_UNIQUE_ID]; // => "block"
const { manifest } = deidentify(
{ loci: [{ path: "PID-11.5", kind: "zip", category: SAFE_HARBOR_CATEGORIES.GEOGRAPHIC, value: "90210" }] },
{},
);
manifest[0]?.transform; // => "generalize"
Codes and categories lists the transform each preset assigns to every category.
Derive a policy of your own
Derive a custom policy with defineDeidPolicy: you can only ever deviate from the safe default,
never forget a category, because every category you do not name keeps its Safe Harbor transform.
import { defineDeidPolicy, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
// A research policy that date-shifts instead of generalizing (Expert-Determination-supporting).
const research = defineDeidPolicy({
name: "research",
transforms: { [SAFE_HARBOR_CATEGORIES.DATES]: "date-shift" },
});
research.transforms[SAFE_HARBOR_CATEGORIES.DATES]; // => "date-shift"
research.transforms[SAFE_HARBOR_CATEGORIES.NAMES]; // => "redact"
A policy that keeps the safe-harbor name may not assign a transform whose output is derived from
the value (date-shift, pseudonymize, hash, or generalize outside geography and dates): it is
refused with DEID_POLICY_INVALID. Name a policy like the one above distinctly.
Use a profile
A profile is a named, reusable preset: a policy plus the retention classes it may keep, a default redactor, and the standard it declares. Two ship with the library:
SAFE_HARBOR_PROFILE: the fail-closed default, with no keyed transform and nothing retained.LIMITED_DATA_SET_PROFILE: a research preset that date-shifts dates and keeps a consistent keyed surrogate for the medical record, health plan beneficiary, and account numbers. It is not Safe Harbor, it requires a key, and it does not on its own make a Limited Data Set. Known limitations states exactly what it keeps.
Pass a profile through profileOptions, which is what carries its retention set; options built by
hand from profile.policy alone retain nothing. Derive a site profile with defineDeidProfile. Its
contract is widen, never narrow: a site may move a category to an equal-or-stronger transform,
never a weaker one.
import {
defineDeidProfile,
profileOptions,
SAFE_HARBOR_PROFILE,
SAFE_HARBOR_CATEGORIES,
} from "@cosyte/deid";
// generalize -> redact removes more, so it is allowed.
const strict = defineDeidProfile({
name: "site-strict",
transforms: { [SAFE_HARBOR_CATEGORIES.GEOGRAPHIC]: "redact" },
});
strict.policy.transforms[SAFE_HARBOR_CATEGORIES.GEOGRAPHIC]; // => "redact"
profileOptions(SAFE_HARBOR_PROFILE).policy === SAFE_HARBOR_PROFILE.policy; // => true
import { defineDeidProfile, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
// redact -> generalize would keep part of a name: DEID_PROFILE_INVALID.
defineDeidProfile({
name: "site-loose",
transforms: { [SAFE_HARBOR_CATEGORIES.NAMES]: "generalize" },
});
Keep records linkable without leaking
Pseudonymize identifiers with a consumer-held key so the same MRN maps to the same surrogate everywhere, without the surrogate being reversible. This is not Safe Harbor: a surrogate derived from the individual's own value is a re-identification code §164.514(c)(1) does not permit, so the built-in Safe Harbor policy removes those identifiers instead, and keyed surrogates live behind a preset that does not claim the label. Keep a patient linkable across documents shows how.
Read the manifest
Every action is recorded value-free. Watch for DEID_LOCUS_BLOCKED / DEID_FREETEXT_BLOCKED (the
fail-closed decisions), DEID_RESIDUAL_RETAINED (a kept year or safe 3-digit ZIP), and
reidentificationCode: true (a keyed surrogate): the residuals you surface to a human for the
§164.514(b)(2)(ii) actual-knowledge test and for an Expert Determination.
import { deidentify, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
const { manifest } = deidentify(
{
loci: [
{ path: "PID-7", kind: "date", category: SAFE_HARBOR_CATEGORIES.DATES, value: "1985-07-02" },
{ path: "ZPI-1", kind: "unknown", value: "ZZVENDORVALUE" },
],
},
{},
);
manifest.map((entry) => `${entry.locus} ${entry.disposition} ${entry.code}`); // => ["PID-7 transformed DEID_RESIDUAL_RETAINED", "ZPI-1 blocked DEID_LOCUS_BLOCKED"]
Codes and categories lists every disposition code. To hand the residuals to a qualified expert, build a support report.
Find the guide for your format
- De-identify an HL7 v2 message
- De-identify a C-CDA document
- De-identify a FHIR resource
- De-identify an X12 interchange
- De-identify an NCPDP Telecom transaction
- De-identify a DICOM file