One Word in a Spec Cost Us a GDPR Violation — Because the Agent Never Asked What We Meant
Why this matters
Your agent will never pause and ask what you meant. It reads the sentence, picks an interpretation, and ships code. Which means every fuzzy word you leave sitting in a spec doesn't stay fuzzy — it turns into a decision, made on your behalf, silently, somewhere in a diff you'll probably never read.
The incident: one word, two meanings
The ticket said: "remove the user record when the account is closed." The agent read "remove," scanned the codebase, found a `deleted_at` column already in use elsewhere, and did a soft delete — flip the flag, keep the row. Perfectly reasonable. Consistent with the rest of the repo, even. And wrong, because this was the account-closure path, and three weeks later a GDPR erasure request landed for one of those exact users. The row was still sitting there. The data was still sitting there. Nobody could explain why — because nobody had actually decided "soft delete." The agent had, quietly, while patching a hole nobody knew was open.
What a human teammate would have done
Hand that same sentence to a human engineer and watch what happens: they stall for about two seconds, then fire off a Slack message — "soft or hard delete on this one?" That's the entire fix. Two minutes, and the mild discomfort of asking something that feels like it should be obvious. Agents skip that step entirely. They don't feel the friction of an underspecified sentence; they just grab the nearest signal — existing code patterns, naming conventions, whatever's statistically common — and keep moving. The pause-and-ask a human does automatically is something you now have to engineer into the spec itself, because the reader on the other end isn't going to supply it for free.
Reread the spec: which word was doing two jobs
Go back to the actual sentence: "remove the user record when the account is closed." All the risk lives in one word: "remove." To whoever wrote it, that obviously meant "make this person's data disappear, for real" — a totally normal thing to mean in plain English. But "remove" is quietly doing two jobs at once. It names an outcome (user shouldn't show up anywhere) while also implying a mechanism — hard delete, tombstone, soft delete, anonymize — without ever committing to one. It read as self-evidently fine precisely because the author had already resolved the ambiguity in their own head. That's the trap: the person closest to the intent is the worst-positioned to spot the gap, because their own brain already filled it in before they finished typing the sentence.
The reframe: judgment calls vs. unhandled inputs
A spec written for a human is allowed to have judgment calls baked into it — that's not a defect, it's what makes a teammate a teammate instead of a script. They bring context, they ask when something doesn't line up, they catch the thing you forgot to spell out. A spec written for an agent has to treat that exact same judgment call as an unhandled input, because the agent will never surface it — it just picks a branch and runs. Spec-driven development for agents isn't "write clearer prose." It's finding every spot where you were leaning on a human's judgment and replacing it with an explicit call, before the agent makes that call for you.
The technique: a gap audit
Walk every noun and verb in the spec and ask: what are the two ways someone could read this, and did I actually pick one on paper? Run that check systematically across the categories that bite people again and again:
- ▹Delete/removal semantics — soft delete, hard delete, tombstone, cascade to related records, anonymize?
- ▹Error handling — what happens on partial failure, invalid input, or a downstream timeout — retry, fail loudly, fail silently?
- ▹Idempotency — if this runs twice (retry, duplicate webhook, replayed event), is that safe, and what should the second call actually do?
- ▹Concurrency — two requests hitting the same record at once: last-write-wins, lock, reject the second one?
- ▹Defaults — every optional field or unstated case needs an explicit default, not one you're just assuming.
If you can't immediately answer "what happens if X instead of Y" for a given noun or verb, that's a gap. Close it in the spec, in writing, before the agent closes it in code.
Before / after
BEFORE (spec for a human, works fine for a human):
Remove the user record when the account is closed.
AFTER (spec for an agent, gap closed):
When an account is closed:
- Hard-delete the user row from `users` immediately (not a soft delete —
this path must satisfy GDPR erasure; do not reuse the `deleted_at`
pattern used elsewhere in the codebase).
- Cascade-delete dependent rows in `sessions`, `preferences`,
`audit_log_pii` only (leave anonymized `audit_log_actions` intact).
- If the delete fails partway (e.g. FK constraint on an unexpected
table), roll back the whole operation and raise — do not leave a
partially-deleted user.
- If this handler is invoked twice for the same user_id (retry,
duplicate event), the second call should no-op with a 200, not error.
- No concurrency concern: account closure is user-initiated and
single-writer per user_id.The tell for 'done'
You know a spec is actually finished when you can answer, off the top of your head, what the agent would do for a handful of adversarial inputs — double-submit, partial failure, a blank field, a record that doesn't exist — without opening the code it generated. If you have to go read the diff to find out what actually happened, the spec wasn't a spec. It was a draft with the interesting parts left as an exercise for the agent.
Where this sits in the course
This isn't a prompting trick. Better wording doesn't fix an unresolved judgment call — only a decision does. Day 7 is a writing discipline: treat your own spec as adversarial input before the agent does. Next lesson takes this gap audit and turns it into something you can actually run against the agent — a test, not just a checklist — so "did I close the gap" stops being a feeling and becomes a verifiable answer.
Extend your knowledge
- ▹Pick one recent spec you wrote for an agent and run the gap audit against it — highlight every noun/verb, flag the ones with two plausible readings, and check whether you actually resolved them on paper.
- ▹Look at a bug from an agent-generated PR in your own repo: trace it back to the spec line that left the gap open, the way 'remove' did here.
- ▹Read up on GDPR 'right to erasure' requirements if you're not familiar — it's a good forcing function for practicing precise delete semantics in specs.
- ▹Next lesson (Day 8) turns this gap audit into a testable artifact — worth revisiting this post once that lands to compare the manual checklist against the automated version.
Discussion
Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.