Read two essays in this section together and they describe one machine.
Anonymization Is Not in the Rule. The Test Is. concluded that scrubbing a prompt to the standard of Comment [4] to Rule 1.6 — “no reasonable likelihood that the listener will be able to ascertain the identity of the client or the situation involved” — is the one route to a hosted model that works no matter the privilege, no matter the harm, and no matter whether the client has signed anything. Apple Just Priced the Local Thesis pointed out that the hardware to run a capable model on a desk now starts at $899. And the June piece, Running Models on Your Own Hardware, argued that local models are close to the frontier on narrow, read-heavy work and a real step behind on hard reasoning.
Put those three together and the design falls right out. The local model does what it’s good at: reading raw client text and finding what identifies. The frontier model does what it’s good at: reasoning, on a slice it can’t trace back to anybody. The token map connecting the two never leaves the office. And a person stands at the one spot where information crosses the firm’s boundary.
It’s a design. Here’s what each stage has to do, the code for the mechanical half, and exactly how each stage breaks.
The pipeline
Step 0. Is the matter even eligible?
Three questions come before any software runs.
What did the client pick? In Run It Yourself, Then Let the Client Choose I described a tier election where a Tier 1 client gets “[o]nly models running on the firm’s own hardware. No information relating to the representation is transmitted to any outside provider.” This pipeline doesn’t run on a Tier 1 matter, and scrubbing doesn’t change that. A scrubbed prompt built from a matter is still information relating to the representation; Comment [3] reaches that information “whatever its source.” Comment [4] makes a well-built hypothetical OK to disclose. It doesn’t make the hypothetical stop coming from the client’s matter. The client’s election covered the information, not just the names, and the routing has to honor what the client actually chose.
Has the client told you anything? Comment [17] contemplates that “[a] client may require the lawyer to implement special security measures not required by this Rule”, and Comment [5] makes implied authority subject to “the client’s instructions”. An instruction to keep the matter off outside services ends it right there, same as a Tier 1 election.
Can this job be scrubbed at all? Full-record review, reading a production for the one document that matters, a deposition summary, a medical chronology: those jobs are the specifics, and once you strip the specifics there’s nothing left to analyze. They go to the local model alone, or to a hosted model with the client’s informed consent. The pipeline is for the question you can ask as a hypothetical — the legal issue, how the argument hangs together, a draft built from a fact pattern you can tell without the identifying details.
Step 1. Build the matter’s register
The exact-match pass works off a list, so the list comes first. For each matter: every party’s full name and the short versions people actually use; relatives, employers, and household members, because federal health-privacy law lists identifiers of “relatives, employers, or household members of the individual” for a reason; street addresses; account, claim, policy, and loan numbers; the court file number; the judge; opposing counsel; and any dollar figure or date that also shows up in a public filing.
Build it once from the file, keep it with the matter, and update it when the file changes. The register is client information too, and it lives where the map lives.
Step 2. The exact-match pass
Before any model reads anything, exact matching takes out whatever exact matching can take out. It’s fast, you can audit it, and unlike a model, it doesn’t have good days and bad days. Here’s the core of it:
import json, re, secrets
from pathlib import Path
# Path fragments a sync client is known to use. A floor, not a proof.
SYNCED = ("Dropbox", "OneDrive", "Google Drive", "Mobile Documents", "CloudStorage")
PATTERNS = { # identifiers no party list will contain
"SSN": r"\b\d{3}-\d{2}-\d{4}\b",
"ACCT": r"\b\d{8,17}\b",
"PHONE": r"\(?\b\d{3}\)?[-. ]\d{3}[-. ]\d{4}\b",
}
def new_token(kind, tmap): # random, never derived from the value,
used = set(tmap.values()) # and never a token already in the map
while True:
token = f"[{kind}_{secrets.randbelow(10**6):06d}]"
if token not in used:
return token
def pseudonymize(text, register, tmap):
"""register: {"PARTY": [...], "ADDRESS": [...], ...} for ONE matter."""
items = [(k, v) for k, vals in register.items() for v in vals]
for kind, value in sorted(items, key=lambda kv: -len(kv[1])): # longest first
if value not in tmap:
tmap[value] = new_token(kind, tmap)
token = tmap[value]
text = re.sub(rf"(?<!\w){re.escape(value)}(?!\w)", token, text, flags=re.I)
for kind, pat in PATTERNS.items():
for value in set(re.findall(pat, text)):
if value not in tmap:
tmap[value] = new_token(kind, tmap)
token = tmap[value]
text = text.replace(value, token)
return text
def residue(text, register):
"""Run AFTER every pass, including the local model's. Non-empty = stop the line."""
hits = [v for vals in register.values() for v in vals
if re.search(rf"(?<!\w){re.escape(v)}(?!\w)", text, flags=re.I)]
return hits + [m for p in PATTERNS.values() for m in re.findall(p, text)]
def reidentify(text, tmap):
back = {tok: val for val, tok in tmap.items()}
if len(back) != len(tmap): # two values share a token: refuse
raise ValueError("token map contains duplicate tokens; re-identification would swap people")
lookalikes = set(re.findall(r"\[?\b[A-Za-z]+[_ ]?\d{6}\b\]?", text))
stray = lookalikes - set(back)
if stray: # a token the model invented or mangled
raise ValueError(f"unmapped tokens in output: {sorted(stray)}")
for tok, val in back.items():
text = text.replace(tok, val)
return text
def save_map(tmap, path):
p = Path(path).expanduser().resolve()
if any(s in str(p) for s in SYNCED):
raise SystemExit(f"refusing to write a token map under a synced folder: {p}")
p.write_text(json.dumps(tmap, indent=1))
Four details in that code are the difference between a pass that works and one that just looks like it works. Each one is a way the naive version breaks on an ordinary test file:
- Longest first. “Jane Q. Example” has to be replaced before “Jane,” or the output reads “[PARTY_…] Q. Example.”
- Word boundaries. Without them, a register entry of “Jane” turns a witness named Janet into “[PARTY_…]t,” and the residue check flags a name that isn’t there.
- Random, unique tokens. Initials or a client number are abbreviations. They aren’t pseudonyms. HIPAA’s re-identification provision, 45 C.F.R. § 164.514(c)(1), states the principle: a re-identification code must be “not derived from or related to information about the individual”. Random tokens bring their own problem: a new token can collide with one already issued, and a map where two names share a token re-identifies one party as the other without throwing any error.
new_tokenkeeps drawing until it gets a token the map doesn’t already hold, andreidentify()refuses any map that contains a duplicate. - Tokens a machine can spot. Every token carries six digits, so a token the frontier model invents or mangles (“Party 482913,” “[PARTY_999999]”) gets caught on the way back. Mix letters and digits in the token format and the same check flags ordinary words as stray tokens. What it won’t catch is a token with a dropped digit, which is one reason a person still reads the result.
Step 3. The local model pass
A register catches what somebody thought to write down. It won’t catch “her brother-in-law, the county sheriff,” a nickname nobody recorded, a business described by what it does (“the only cold-storage warehouse in town”), or a medical condition rare enough to point straight at whoever has it. That’s reading comprehension, and it’s the local model’s job.
Give the local model two jobs on the already-tokenized text: mark every span the register missed that could identify a person, place, or organization, and propose a generalization for every date, amount, and location that isn’t already a token. The model suggests. It doesn’t decide. Its markings go to the person in Step 6, and its rewritten text goes right back through the residue check in Step 4.
This pass has to be local because it reads the raw text. That’s the whole reason the design has two models instead of one. It’s also exactly the kind of work the June essay said local models do nearly as well as frontier ones: long input, short output, a job with a right answer. So test it that way. Seed thirty real-looking documents with identifiers you planted, run the pass, and count how many it catches. How Do You Know It Works? set the rule for extraction: “Weight recall far above precision.” Here a false alarm costs somebody five seconds of reading. A miss is a disclosure.
Step 4. Check for residue, again
Run residue() on whatever the local model hands back. Models rewrite, and a model told to “generalize the location” will sometimes put a name back from context while it’s at it. Anything on the register that shows up again stops the line. The check is cheap, it’s mechanical, and it runs every time, because Step 2 is pointless if Step 3 can quietly undo it.
Step 5. Generalize the situation
Software can’t finish this step, because it turns on what the question actually needs.
Go through every exact value left in the text and ask whether the answer depends on it. A severance amount that only sets the scene becomes “a five-figure severance.” A termination date that only puts events in order becomes “nine days after the report.” A county that only places the story becomes “a rural county.” Those generalizations are what move a prompt from “no names in it” to “no way back to the situation.”
When the exact value does drive the answer, don’t send it. If the question is whether a deadline has run, compute the deadline on your desk and send what’s left of the question. If it’s whether a claim clears a threshold, answer that locally and send the legal issue without the number. When one value both identifies the client and decides the question, you’ve hit the pipeline’s limit, and it’s local-only or consent.
Step 6. A person reads what’s about to leave
In The Agent Drafts. The Human Sends. I set the rule for this firm’s stack: “A human fires anything that crosses the firm’s boundary.” A scrubbed prompt headed to a hosted model is exactly that kind of crossing, and it gets a person.
The reader asks one question: could a stranger with this prompt and a search engine find the client? And the reader reads the whole conversation that’s about to go, not just the newest turn, because the model gets the whole thread, and twelve harmless turns can add up to one identifiable fact pattern.
Sometimes the answer is “this cannot be de-identified.” That’s a perfectly good answer. It sends the job to the local model alone, or to an informed-consent conversation under Rule 1.0(f), which requires “adequate information and explanation about the material risks of and reasonably available alternatives to the proposed course of conduct.”
Step 7. Send the smallest slice, in a fresh session
Send only what the question needs, to a provider whose terms you’ve read. Opinion 512 makes that the baseline at page 7: “all lawyers should read and understand the Terms of Use, privacy policy, and related contractual terms and policies of any GAI tool they use”. Rule 5.3’s Comment [3] ties the diligence to “the terms of any arrangements concerning the protection of client information”.
Start a fresh session for every question. A long-running thread about one matter is the pile-up problem from Step 6 built right into the setup. Separate questions in separate sessions give each listener one piece.
Step 8. Check the answer before anybody re-identifies it
reidentify() refuses to run if the answer has a token that isn’t in the map. That catches a model that invented “[PARTY_999999]” to name somebody it assumed exists, and a model that re-cased or re-spaced a token so it won’t map back. Refusing is the right call: an answer about a party who doesn’t exist is a wrong answer, and quietly leaving the stray token in place would hide that.
What code can’t catch is a model that guesses. An answer that says the facts “resemble” a reported case, or names a likely employer, has put identity back in words. Whoever reads the output reads it for that too.
Step 9. Re-identify on the desk
The answer gets re-identified locally, into the matter’s own working file, by somebody authorized to hold the map. Re-identified text never goes back into the hosted session. The most common way a clean pipeline leaks is a follow-up typed with the real name in it, because the lawyer has the real answer open in the next window.
Where the map can’t live
The map is the most sensitive file this whole thing creates. It’s the key to the scrubbed prompt, and anybody holding the map and the vendor’s copy of the prompt holds the original. HIPAA’s re-identification provision makes the same point for health data: the entity can keep a code, provided it “does not disclose the mechanism for re-identification.” The rules for the map come out of Rule 1.6(c)’s “reasonable efforts to prevent the inadvertent or unauthorized disclosure of, or unauthorized access to, information relating to the representation of a client”:
- Never in a folder a sync client watches. The
save_mapguard refuses the obvious paths. It only catches the obvious ones, because some sync products sync ordinary-looking folders like Documents and Desktop with no telltale path at all. Keep maps in one directory you’ve confirmed no sync client watches, and check it again after every operating-system update. - One map per matter, never one for the firm. A firm-wide map is every client’s identity in one file.
- Encrypted at rest, and backed up only to an encrypted backup the firm controls. Lose the map and the scrubbed work is stranded. Leak it and all of it comes undone.
- Limited to the people on the matter. Comment [5] lets lawyers in a firm share client information “unless the client has instructed that particular information be confined to specified lawyers.” A map doesn’t need to go as far as the rule allows, and it shouldn’t.
- Kept and destroyed with the file. The duty of confidentiality outlives the representation — Comment [19]: “The duty of confidentiality continues after the client-lawyer relationship has terminated.” — and the map goes wherever the closed file goes.
The records this creates
Run this pipeline and you’ve got three records where you used to have one: the vendor’s copy of the scrubbed prompts and answers, kept on the vendor’s terms; the firm’s local copy of the same exchange; and the map. On its own, the vendor’s copy is tokens and generalizations. Put the firm’s copy together with the map and you’ve got everything.
That’s the point. A subpoena served on the provider gets fact patterns about “[PARTY_…].” A subpoena served on the firm gets the whole record, which is where a subpoena for a lawyer’s work ought to have to go. But the firm’s copy is now a complete, timestamped record of counsel’s questions about the matter, and everything I wrote in The Discovery of Your Prompts applies to it as-is: pick the retention period on purpose, make sure a litigation hold can override it, and don’t let the pipeline’s logs turn into one more folder nobody’s ever inventoried.
How it breaks, and what catches it
| Failure | What catches it | What still gets through |
|---|---|---|
| The register misses a name | Local model pass (Step 3); human read (Step 6) | A name neither the model nor the reader recognizes as a name |
| The local model misses an identifier or restores one | Residue check after the model (Step 4) | Anything not on the register; that is why Step 6 exists |
| The situation identifies the client with no name in it | Generalization (Step 5); the stranger-with-a-search-engine test (Step 6) | A reader who knows the facts too well to see them as a stranger would |
| Harmless turns accumulate into an identifiable thread | Fresh session per question (Step 7); reading the whole thread (Step 6) | A single question that genuinely needs all the facts at once; that one is not a candidate |
| The map syncs to the cloud | save_map path guard; one audited directory |
A sync client configured to watch an ordinary folder |
| The answer re-attaches identity | reidentify() refusing unmapped tokens (Step 8); human read of the output |
A mangled token with a dropped digit; a guess expressed in words |
| Re-identified text goes back to the vendor | Re-identify only on the desk, never in the session (Step 9) | A lawyer typing the real name into a follow-up |
| Logs outlive the policy, or die before a hold | A written retention period the hold process can override | Nothing, if the policy exists and is applied |
Every row leaves something in the last column, and every one of those is a person’s judgment call. The design puts that judgment in two places, the outgoing read and the incoming read, which is where a small firm can actually afford to spend it.
What it costs, in the terms Comment [17] uses
Comment [17] says the factors for judging whether Rule 1.6(c) efforts were reasonable “include, but are not limited to,” these: “the sensitivity of the information, the likelihood of disclosure if additional safeguards are not employed, the cost of employing additional safeguards, the difficulty of implementing the safeguards, and the extent to which the safeguards adversely affect the lawyer’s ability to represent clients”. Here’s the pipeline scored against each one.
| Factor | Effect of the pipeline |
|---|---|
| Sensitivity of the information | What the vendor holds is tokens and generalized facts; the sensitive part stays on the desk |
| Likelihood of disclosure without the safeguard | Falls sharply for identity; unchanged for anything that could not be generalized, which is why that material does not go |
| Cost | Hardware from $899 at the entry level; setup time; a few minutes of a person’s reading per outgoing prompt |
| Difficulty | Real. A register per matter, a local model to maintain, and an evaluation set for the de-identifier itself |
| Adverse effect on the ability to represent clients | The real cost. Generalization loses precision, the extra steps add time, and some tasks cannot use the pipeline at all |
The last row is where a skeptic ought to push, and he’d be right to. A pipeline that makes the frontier model’s answer materially worse has traded one client interest for another. Measure it: run the same questions with and without the pipeline, on fact patterns you made up for the purpose, and compare the answers. If generalizing is costing you the answer, the pipeline’s telling you that question needed the specifics, and specifics mean local or consent.
What this doesn’t buy you
It doesn’t satisfy a Tier 1 election, and it doesn’t satisfy a client’s instruction to keep the matter in the building. That’s what Step 0 is for.
It doesn’t make anything less privileged. Privilege is about the communication, and a scrubbed copy of a privileged communication still came from one. The pipeline goes to Rule 1.6’s disclosure question. It doesn’t answer a waiver question, and nobody should describe it like it does.
It does nothing about hallucination. The frontier model’s answer is exactly as reliable as it would’ve been with the names in it, and every authority in it gets checked against the source before anyone relies on it.
And it doesn’t replace consent for the work you can’t scrub. What it does is shrink that pile down to the jobs that really need the specifics, so the consent conversation is about the work that needs it instead of boilerplate about everything.
I’ve got nothing to sell here. The code above is free to copy and adapt, which is the whole idea of this section. My interest is the one I disclosed in the anonymization essay: this firm uses these tools, and a design that makes them easier to use defensibly helps it.
When I wouldn’t bother
- When the volume’s low. If you ask a hosted model a handful of questions a month, write each hypothetical by hand, read it, and send it. The pipeline earns its keep at volume.
- When the matter’s Tier 1, or the job can’t be scrubbed. Local alone, or consent. No amount of engineering changes that.
- When the scrubber flunks its test. If the local pass misses planted identifiers at a rate you wouldn’t accept from a paralegal, it doesn’t ship, and it doesn’t matter how good the frontier answers look.
- When local models catch up on reasoning. The design exists because the best reasoning is remote. If a model on the desk gets good enough for the hard part, the right setup is the simpler one: nothing leaves, and the pipeline gets retired. The July essay, The Model Will Be Free. The Electricity Won’t., argued that day is coming. When it gets here, I’ll be glad to throw this away.
Sources
- Minn. R. Prof. Conduct 1.6 — paragraph (c) (reasonable efforts); Comment [3] (“whatever its source”); Comment [4] (the hypothetical test); Comment [5] (implied authority subject to client instructions; confinement to specified lawyers); Comment [17] (reasonableness factors; client-required security measures); Comment [19] (duty survives termination)
- Minn. R. Prof. Conduct 1.0 — paragraph (f) (informed consent: material risks and reasonably available alternatives)
- Minn. R. Prof. Conduct 5.3 — Comment [3] (outside services; terms of arrangements protecting client information)
- ABA Standing Committee on Ethics and Professional Responsibility, Formal Opinion 512 (July 29, 2024) — p. 7 (read and understand the terms of any GAI tool used)
- 45 C.F.R. § 164.514 — paragraph (b)(2)(i) (identifiers of relatives, employers, and household members); (c)(1) (re-identification code not derived from the individual’s information); (c)(2) (mechanism for re-identification not disclosed). Cited as an analog; it does not govern lawyers’ confidentiality duties
- Companion essays in this section: Anonymization Is Not in the Rule. The Test Is., Apple Just Priced the Local Thesis, Running Models on Your Own Hardware, Run It Yourself, Then Let the Client Choose, The Agent Drafts. The Human Sends., How Do You Know It Works?, The Discovery of Your Prompts, and The Model Will Be Free. The Electricity Won’t.
General commentary on practice management and legal technology. Not legal advice and not ethics advice. The code is a sketch offered for adaptation, not a certified de-identification tool; test it against your own data before relying on it. Every fact pattern and name in this article is invented. No client information appears in this article.