- The shape of the bug
- Two deeper problems
- Guardrails for any TOCTOU-shaped AI failure
- Check the board before you move
It was a hot summer day in Chennai, the city of India’s chess prodigies Prag, Gukesh and Vaishali. Like many other parents here, I had taken Adhiyan (my son) out for a day-long chess tournament. Barely managing to beat the heat in the waiting room, I pulled up my laptop to finish something I had started before the long weekend.
Last week, a new business requirement had come in from one of our flagship customers. Rather than rejiggling anyone else’s priorities, I picked it up myself, opened a Claude Code session, and started working through the implementation. Along the way I asked Claude to fetch an OpenSpec page from our internal wiki so I could update the spec later. Claude read the page and held the full body in conversation context. Then the long weekend happened, and the session sat there, context frozen in time.
Now, Sunday in the waiting room between rounds, I picked up where I left off. “Update the page with the new field row.” Claude inserted the row and updated the page. I closed the laptop and went back to watching chess.
A few hours later, a Slack ping from a colleague: “Looks like you accidentally deleted an entry from the spec. Was this done using Claude?”
It was. While I was away, a teammate had edited the same wiki page on Friday. His change landed as the next version. My session was still holding Thursday’s version. When Claude updated the page on Sunday, it submitted Thursday’s body with my row added on top. Everything my teammate had done on Friday was silently overwritten. Another colleague caught it and fixed it.
The shape of the bug
If you have done any systems programming, you have seen this before. It is a TOCTOU bug (Time of Check to Time of Use). A program reads some state, acts on it later, and between the two someone else changes the state underneath. The textbook example is file system operations in Linux. Mine was a wiki page, and the window between “check” and “use” was three days.
From the model’s perspective, content read 30 seconds ago and content read 3 days ago look identical. Both are just text in the conversation. There is no freshness signal.
This was a small change to a wiki page. The cost was 30 minutes of someone else’s work. But the failure mode scales. I have skills (automated workflows) running production-adjacent workloads through Claude. If the same stale-context pattern hits a config read on Monday and an action on Wednesday, the blast radius is not a missing field. It is a misconfigured pipeline or a wrong computation against live data.
Two deeper problems
-
The tooling bypasses its own safety valve. Many APIs support optimistic locking: you pass the version number, and if someone else incremented it, you get a 409 Conflict. The mechanism exists. But MCP tool wrappers often auto-increment the version at write time, so the check never fires. The safety valve is in the protocol; the tooling just routes around it. This pattern likely exists in other tool integrations too.
-
The documentation lives outside version control. In this case, the spec was maintained on a wiki page, and that is where both my colleague and I made our edits. If this documentation lived alongside the code in the repo, the overwrite would have been a merge conflict in a PR: visible, attributed, resolvable. Instead it was a silent last-write-wins on a wiki page with no branching, no diffs, and no review.
Guardrails for any TOCTOU-shaped AI failure
The specific issue was a wiki page, but the pattern applies to anything an AI agent reads from an external system and later writes back: wikis, issue trackers, config stores, databases, APIs.
-
Re-read before every write. Before any external mutation (
update*,edit*,create*that depends on prior state), re-run the matchingget*call in the same response. Use the freshly fetched content as the base. Never reuse content fetched earlier in the conversation. One extra API call would have prevented this entirely. -
Prefer targeted updates over full-body overwrites. When the API supports it, send only what changed: a field update, a patch, an append. A targeted update leaves concurrent edits in untouched parts intact by construction. A full-body overwrite submitting a stale snapshot cannot.
-
Treat conversation context as a cache that expires. External systems have concurrent writers. Your read is a snapshot at a point in time, and another write can land between your read and your write. Your conversation memory is not authoritative for anything you did not write yourself.
-
Demand version-aware tooling. If the underlying API supports optimistic locking (version numbers, ETags,
If-Matchheaders), the tool layer should enforce it, not silently bypass it. Every MCP tool that wraps a read-then-write API should carry the version forward from read to write and let the API reject stale writes.
Check the board before you move
Adhiyan uses a calculation trainer for chess. It works like this: you see a frozen position and calculate the best sequence 3–4 moves ahead. Great for building depth of thought, but the position on screen never changes while you think. A real game is different. The opponent moves, the board shifts, and you have to look at what actually happened before you decide your next move.
Claude is excellent at calculating from a frozen position. The re-read rule is my way of teaching it to check the board first before its next move.
If you have hit similar stale-context failures, or have guardrails in your CLAUDE.md or agent workflows that address this class of problem, I’d love to hear about them.