AI agents
Anthropic's Fable retention: a legaltech incident replay
The morning Anthropic's Fable retention policy changed, a Rotterdam legaltech had four hours to rewrite a purge job that had quietly been a no-op.

07:14 on a Wednesday. The head of platform at a 23-person legaltech SaaS in Rotterdam opens Hacker News in bed, sees the headline Anthropic requires 30 day data retention for Fable and Mythos, and his thumb stops moving. Their product, a contract-review agent used by mid-sized Dutch law firms, has been quietly piping fourteen months of attorney-client transcripts through Anthropic's zero-retention enterprise tier. Their data-protection addendum with every customer says, in writing, that no transcript content is stored at any third-party processor beyond the call lifetime. By 09:00 the company has an incident channel, by 11:00 a draft notification to two of their bigger firms, and by 17:30 a rewritten purge job in production.
This is what that day actually looked like, and what the on-call engineer would do differently if it happened next month.
How ZDR was load-bearing
The product is a small one. Three backend engineers, one frontend, two ML, and a CTO who still reviews every Postgres migration. They use an agent loop to read draft contracts, ask the lawyer follow-up questions in a chat window, and produce a marked-up redline. Each conversation runs 40 to 90 turns before it terminates.
Zero data retention with Anthropic was load-bearing for three things:
- The DPA they sign with every law firm. Article 4 names Anthropic as a sub-processor and asserts that no prompt or response content is retained server-side beyond the call.
- The architecture diagram in their SOC 2 Type II audit, currently under review.
- Their internal purge job, which only had to drop transcripts from their own Postgres after the matter closed. The job assumed the third-party side was already a no-op.
For fourteen months, every one of those was true. Then it wasn't.
What the announcement changed
The HN thread was misread by half the legaltech founders we know in the first hour. The change is narrower than the headline reads: it applies to two specific newer model families and is intended to support abuse-monitoring on agentic tooling. The same week, a separate front-page thread had cybersecurity researchers complaining loudly that the guardrails on Fable were too tight for legitimate red-teaming, which only made the policy churn feel more chaotic to anyone scanning headlines.
The narrow scope saved the Rotterdam team a worse Wednesday. It did not save them all of Wednesday. Their last sprint had migrated the redline agent onto a Fable preview because tool-use performance on PDF tables was meaningfully better, and the migration was 60% rolled out behind a feature flag. About 31% of live traffic was already hitting a model that, as of that morning's update, would retain transcript content for 30 days.
The relevant clause in their customer DPA was the standard Article 28 GDPR processor language, which puts the obligation on the controller (the legaltech) to flow down retention promises to every sub-processor. A vendor's policy page is not a unilateral patch to those contracts.
The purge job that was silently a no-op
Here is the function that had been running every night at 02:00 Amsterdam time, lightly stripped:
def purge_closed_matters(db, anthropic_client):
closed = db.fetch("""
SELECT matter_id FROM matters
WHERE status = 'closed'
AND closed_at < now() - interval '90 days'
""")
for m in closed:
db.execute(
"DELETE FROM transcripts WHERE matter_id = %s",
(m["matter_id"],),
)
# third-party side: ZDR, nothing to do
log.info("purge complete", count=len(closed))
That last comment is the entire bug. It was true in March 2025, true through fifteen sprint reviews, and false at 06:00 on the morning of the announcement. The job had no concept of a third-party transcript identifier because none had ever been worth storing. Anthropic's API returns a response id with every message, but the team had dropped them on the floor at the SDK boundary.
Two problems followed from that. First, for any conversation that had been routed to the Fable model, they had no way to enumerate which third-party records existed. Second, even if they had the ids, the actual deletion endpoint requires the workspace admin key, which lived only on the CTO's laptop. Anthropic's own privacy documentation spells out the delete-on-request affordance, but you have to be wired to use it.
The four-hour incident timeline
The team did not start with the rewrite. They started with the customer-facing question: "Do we need to email anyone before lunch?" The CTO walked the timeline back. The feature flag had been on for four days. Two of their twelve customers had traffic routed through the new model. One was the larger firm currently considering a renewal. The decision was made to notify both, plainly, before anyone read it on HN.
The draft email went out at 11:42. It said: we discovered an unintended retention window on a fraction of recent traffic; we have rolled the affected traffic back to the legacy model; we will delete the third-party records within the 30-day window and confirm in writing. No legal hedging, no "out of an abundance of caution" theatre. Both firms replied the same afternoon. Neither raised a complaint. One asked for the deletion confirmation in their Q3 vendor review.
By the time that email went out, the platform team had already disabled the feature flag and routed 100% of inference back to the older model. The 31% number was, helpfully, accurate at the moment of the rollback, because the flag was percentage-based rather than tenant-based.
Rewriting the purge job
The rewrite had to do three things the old job did not: capture the third-party response ids at write time, store them next to the matter, and call the delete endpoint with proper auth at purge time.
Capture happens at the SDK boundary. Every message the agent sends or receives now writes its response id into a small table:
CREATE TABLE third_party_transcript_refs (
id bigserial PRIMARY KEY,
matter_id uuid NOT NULL REFERENCES matters(id),
provider text NOT NULL,
provider_msg_id text NOT NULL,
model text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
purged_at timestamptz
);
CREATE INDEX ON third_party_transcript_refs (matter_id)
WHERE purged_at IS NULL;
The agent loop writes a row per message. The purge job now has something to enumerate:
def purge_closed_matters(db, providers):
closed = db.fetch("""
SELECT m.id AS matter_id
FROM matters m
WHERE m.status = 'closed'
AND m.closed_at < now() - interval '90 days'
""")
for m in closed:
refs = db.fetch("""
SELECT id, provider, provider_msg_id
FROM third_party_transcript_refs
WHERE matter_id = %s
AND purged_at IS NULL
""", (m["matter_id"],))
for ref in refs:
client = providers[ref["provider"]]
try:
client.delete_message(ref["provider_msg_id"])
except ProviderNotFound:
pass # already aged out, treat as success
db.execute("""
UPDATE third_party_transcript_refs
SET purged_at = now()
WHERE id = %s
""", (ref["id"],))
db.execute(
"DELETE FROM transcripts WHERE matter_id = %s",
(m["matter_id"],),
)
log.info("purge complete", matters=len(closed))
The provider map injects a different client for each model family, so a future second sub-processor (or the move back onto a no-retention tier when one ships) does not require another rewrite. The ProviderNotFound branch matters: if a record has already aged out of the 30-day window, the delete call returns 404, and that is the success case, not the failure case.
Two extras shipped the same afternoon. A daily job alarms if the count of purged_at IS NULL rows older than 30 days is non-zero. And the workspace admin key now lives in the secret store, with break-glass access logged, so the purge job can run unattended.
Three lessons from one morning
None of these are new. All of them are easier to ignore than to act on.
First, "the vendor handles it" is an architectural assumption with a half-life. ZDR was real, the team's reliance on it was sensible, and the policy still changed in a way that did not require their consent. Anywhere your data-flow diagram has a "nothing stored here" annotation, you want at least a stub of the code that would handle the day that annotation stops being true.
Second, agentic features get rolled out behind percentage flags more often than tenant flags, because percentage flags are easier to write. Percentage flags are the wrong granularity for any traffic governed by per-customer contracts. The team had been meaning to fix this for two sprints. The morning of the announcement was when they stopped meaning to.
Third, the right first move on an incident like this is the customer email, not the code change. The code change is in your control; the customer's perception of how you behave when surprised is not. Send it before HN gets read at the firm.
If your DPA says a sub-processor stores nothing, build the purge path that proves it anyway, before the vendor changes their mind.
A 30-minute audit you can run today
- Grep your codebase for "ZDR", "zero retention", or any comment ending with "nothing to do". Each one is a future incident.
- Open your DPA template. List every sub-processor whose retention behaviour you assert in writing. For each, find the production code path that would delete from that processor on request. If it does not exist, that is the next ticket.
- Check that every feature flag controlling a sub-processor route is tenant-scoped, not percentage-scoped. If you cannot answer "which customers' data hit this provider this week" in under five minutes, you have a flag-shape problem.
- Confirm the workspace-admin credentials for each AI vendor can be used by an unattended job, not just a laptop. Move them into your secret store.
- Write down, in one paragraph, who emails customers first when a sub-processor changes policy mid-sprint. Pin it in the incident channel.
None of this requires a sprint. It requires an afternoon and the willingness to discover that one of your annotations is now a lie.
When we built the contract-review agent for a Dutch legaltech client, the thing we ran into was exactly this: a purge job that quietly assumed a sub-processor would never store anything, and a feature flag at the wrong altitude. We ended up moving third-party id capture into the SDK boundary itself, so adding a fourth or fifth model family later is a config change rather than another emergency. The same pattern shows up across every AI agents project we ship: the deletion path is part of the architecture, not a footnote in the DPA.
One concrete thing to do this afternoon: open your agent's SDK wrapper and confirm you are persisting the provider's response id on every message. If you are not, that is a 20-line patch, and it is the difference between Wednesday morning being an inconvenience and being a customer call.
Key takeaway
If your DPA says a sub-processor stores nothing, build the purge path that proves it, before the vendor changes their mind for you.
FAQ
Does the new 30-day retention apply to every Anthropic model?
No. The announcement was scoped to specific newer model families used for agentic tooling. Older models pinned in production were not in scope, which is why rollback was a viable first move.
What is the practical difference between ZDR and a 30-day retention window?
ZDR means the vendor never persists prompt or response content beyond the call lifetime. A 30-day window means transcripts are stored, encrypted, and deletable on request, then auto-purged after 30 days.
Should I delete every third-party AI record by default?
Only where your DPA or local law requires it. Build the deletion path so the choice is yours, then enforce it on closed matters or on customer request rather than running a blanket sweep.
Is percentage-based feature flagging always wrong for AI features?
Not always, but it is wrong any time the traffic is governed by per-customer contracts. Tenant-scoped flags are the right altitude for routing decisions that touch a sub-processor.
What should the first hour of a sub-processor policy change look like?
Disable the affected route, identify which customers had traffic on it, and draft a plain-language email before any code change. The rewrite can wait an hour. The customer's first read of the news cannot.