overwrite-guard
Skill0 starsChanged 4 months agoMITView on GitHub
Audits whether two people (or a person and an automated process) editing the same record can silently clobber each other — a save endpoint that does read-modify-write with no check that the record hasn't changed since it was read, a version/revision column that's declared in the schema but never actually enforced in the UPDATE's WHERE clause, or an optimistic-concurrency check that exists but compares against the wrong baseline so it never trips. Use when reviewing or adding a PATCH/PUT/"save" endpoint on a shared or collaboratively-edited resource (user profiles, documents, tickets, inventory, settings), or when asked "did my changes get overwritten", "two people edited this and one's changes disappeared", "does this endpoint handle concurrent edits", "audit our optimistic locking / If-Match / version check", or "why didn't the conflict get caught".
In this repository46 other files it gives its agents.
- atomic-ioSkill
- backfill-pilotSkill
- backpressure-guardSkill
- blast-guardSkill
- changelogSkill
- cipher-guardSkill
- clipboard-guardSkill
- clock-sweepSkill
- codebase-tourSkill
- consent-guardSkill
- devlogSkill
- dormancy-guardSkill
- env-detectiveSkill
- erasure-guardSkill
- export-guardSkill
- failover-guardSkill
- git-rescueSkill
- import-guardSkill
- job-wardenSkill
- migration-guardSkill
- pagination-guardSkill
- portability-auditSkill
- pref-guardSkill
- proration-guardSkill
- push-guardSkill
- quote-guardSkill
- reaccept-guardSkill
- readme-forgeSkill
- recall-guardSkill
- residency-guardSkill
- restore-guardSkill
- resync-guardSkill
- rollout-guardSkill
- rotation-guardSkill
- seat-guardSkill
- secret-spillSkill
- security-sweepSkill
- selectall-guardSkill
- ship-itSkill
- skew-checkSkill
- stale-guardSkill
- storm-guardSkill
- sunset-guardSkill
- throttle-guardSkill
- tombstoneSkill
- trail-guardSkill
The file
Files
overwrite-guard
Most "edit this record" code is read-modify-write: fetch the row, let the user change some fields, write the whole thing back. That's fine for a single editor. It silently breaks the moment two editors (two people, or a person and a background job, or two browser tabs) read the same row and then both write, because the second write has no idea the first one happened — it overwrites the row as it understood it, including the fields it didn't touch, based on a copy that's now stale. Nothing errors. Nothing logs a warning. The first editor's change simply isn't there anymore, and the only sign anything went wrong is a user saying "I know I saved that." This is the lost update problem, and it is a correctness bug about two independent requests racing on the same row — not the thread/lock/goroutine kind of concurrency bug, which is a different failure mode in a different part of the stack and already well covered by general code-review practice.
The fix has one shape: make the write conditional on what was actually read. Carry a version counter, a revision number, or a last-modified timestamp from the read to the write, and have the write's own WHERE clause (or an If-Match precondition on a REST PUT/PATCH) require that value to still match. A stale write then touches zero rows, or gets an explicit 409 Conflict / 412 Precondition Failed, instead of succeeding and destroying data. The two ways this goes wrong in practice aren't "nobody thought about it" — they're a check that exists but doesn't actually fire:
- Declared but never enforced. A real, filed bug (
spidermila/MedCoverissue #461) found eight SQLAlchemy models with aversioncolumn specifically documented for optimistic locking — but the column was never registered as the ORM'sversion_id_col, so the ORM never added theWHERE version = :old_versionpredicate to theUPDATE. The column faithfully incremented on every write and protected nothing. Two admins open the same user record at version 5, each edits a different field, both submit within seconds — the secondUPDATEsucceeds unconditionally and the first admin's change is gone, with noStaleDataErrorand no409anywhere in the stack. - Enforced against the wrong baseline. A second real bug (
CoderLambert/react-learning-playgroundissue #133) shows the subtler trap: the revision check exists and looks correct, but the comparison is built wrong. An editor component loads its draft once from the initialquestionprop and keeps editing locally; while it's open, the record advances to a new revision server-side and the component re-renders with the new prop — but the local draft doesn't change. On submit, the code sendsexpectedRevision: question.revision— the just-refreshed prop's revision, not the revision the draft was actually based on. That matches current storage every time, so the compare-and-swap always "succeeds" and the stale draft overwrites the newer data despite an optimistic-concurrency mechanism being present and, on casual inspection, looking like it does its job.
Both are worse than having no mechanism at all, in one sense: a missing check at least looks unprotected if you go looking for one, where a present-but-broken check gives false confidence to anyone who sees the column, the header, or the diff and assumes it works.
Step 1: find every read-modify-write path on a shared record
Trace each PATCH/PUT endpoint, GraphQL mutation, or ORM save()/update() call that loads a record, lets a caller change some subset of its fields, and writes the whole thing (or the changed fields) back. Prioritize resources plausibly edited by more than one actor close in time: user/account profiles, shared documents or tickets, inventory or stock counts, app settings, anything with a "last edited by" field (its existence is itself evidence someone already expected concurrent edits). Exclude writes that don't have this shape at all — a single atomic UPDATE ... SET count = count + 1 WHERE id = ?, an append-only insert, or a queue consumer claiming a row with SELECT ... FOR UPDATE SKIP LOCKED — those aren't read-modify-write races and don't need this audit.
Step 2: check whether a version/revision/timestamp mechanism exists and is actually wired into the write
Look at the schema and the model for anything that looks like it's for this purpose — version, lock_version, revision, etag, or an updated_at the code checks rather than just records. Then check the generated SQL or the framework's own behavior, not just the column's presence: does the UPDATE's WHERE clause actually include it (WHERE id = ? AND version = ?), or is the column declared but the write is still an unconditional WHERE id = ?? This is exactly the MedCover gap — in Rails this means confirming ActiveRecord::Locking::Optimistic is actually active for the column (not just present on the table); in SQLAlchemy it means version_id_col is set in __mapper_args__; in a hand-rolled query it means the predicate is literally in the WHERE clause and the write checks the affected-row count.
Step 3: if a mechanism exists, verify its baseline — not just its presence
For each check found in step 2, trace where the compared value actually comes from on both ends:
- Server side: is the "current" value it compares against fetched fresh at write time (correct), or is it something that could have been refreshed by an unrelated read earlier in the same request (the react-learning-playground trap)?
- Client/caller side: does the value sent back on write genuinely originate from what was read at the start of this edit session (the form load, the GET that populated a local draft) — or could it have been silently replaced by a later refresh, a prop update, a websocket push, or a second tab sharing state, while the edit in progress still reflects the older data?
A check that exists but is fed the wrong baseline reports as "SAFE" to a skim and is functionally identical to having no check, so trace the actual value flow rather than confirming a version field is present somewhere in the request.
Step 4: check what happens when a stale write is actually detected
Confirm the conflict path is real, not decorative: does a version mismatch actually reject the write (409, 412, a raised StaleDataError/OptimisticLockError) and surface something the caller can act on (reload-and-reapply, a merge UI, a clear error) — or does the code catch the exception, log it, and proceed with the write anyway "to not block the user," which reintroduces the exact bug the mechanism was built to prevent?
Report
Lead with the concrete scenario: who/what races on this record, and what happens when they do. BLOCK for a confirmed lost-update path on a resource actually edited by more than one actor — no version/timestamp check at all (step 2), a check wired to the wrong baseline so it never trips (step 3), or a detected conflict that gets swallowed and overwritten anyway (step 4) — especially when the field at risk is high-value (financial, permissions, inventory) or the overwrite is silent with no error surfaced anywhere. CAUTION when a mechanism exists and fires correctly but the UX on conflict is rough (an opaque error with no reload/merge path), when the race window is narrow enough that collisions will be rare but not impossible, or when only some of several write paths to the same resource are protected. SAFE only with evidence: a version/timestamp predicate confirmed present in the actual write (not just the schema), a baseline traced to the real read that started the edit, and a conflict path that rejects and surfaces rather than swallowing.
Boundaries
- Not a thread/async/lock audit — races between threads, goroutines, or async tasks inside one process (deadlocks, missing mutexes, data races on shared memory) are a different failure mode in a different part of the stack, already covered by general code-review practice. This skill is about two independent requests, arbitrarily far apart in wall-clock time, racing on the same stored row.
- Not offline-sync / mobile conflict-resolution territory — a client that's offline for hours or days and needs merge/CRDT logic to reconcile a queue of local mutations against a server that moved on is a substantially harder problem than this skill's scope: synchronous online clients racing by seconds on an ordinary save. Don't reach for this skill's BLOCK/SAFE framing for that case; it needs a real merge strategy, not a version check.
- Not
stale-guard's territory — that skill proves a read path can't serve data that's wrong for the cache key (invalidation, key-scoping). This skill is about a write path silently destroying another write; the data was never cached incorrectly, it was correctly stored and then correctly-looking-but-wrong code overwrote it. - Doesn't pick a concurrency-control strategy from scratch (pessimistic row locking vs. optimistic versioning) — assumes optimistic concurrency is the right shape for the access pattern (the common case for web/API edits) and audits whether it's actually enforced end-to-end, not which strategy a new feature should choose.
- Can't measure how often two real actors actually collide on the same record in production from source alone — say so plainly when severity genuinely depends on real-world edit frequency rather than guessing how rare the race is.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

