What brokeUpdated 3 October 2026
Sync merged the lists and brought back deleted items
Preference sync in one of our apps merged the local and server copies as a union, so an item removed on one device came back from another device's local storage. It was fixed in a code review round and verified with a five-check Playwright scenario. The fix makes the server copy the source of truth at startup.
The short version
What went wrongAt startup the app merged local and server lists as a union, so an item deleted on one device came back from another device's local storage.
How we found itA code review round on 2 July 2026 caught it, and a new five-check Playwright scenario deletes an item on another device and confirms it stays gone.
The rule nowThe server copy is the source of truth at startup. Seed from local only when the server has nothing, never after a network error, and keep one shared sync hook.
This post documents something Claude got wrong. The claim rests on a commit or a file, not on memory. All of Claude's mistakes
One of our apps keeps two personal lists: favorites and owned items. Both lists live in the browser’s local storage and are also synced to the server, so they follow you between devices. A code review round logged on 2 July 2026 fixed a sync bug that could undo deletions. Remove a favorite on one device, and it could come back from another device’s local storage.
Who made the mistake
Henning writes no code himself, so the original sync logic was Claude’s, and so was the mistake.
What went wrong
The commit message sums up the cause in one line: at startup the app merged the local and server copies, and that union “revived deletions from other devices”. The progress log for that round describes it from the user’s side: a favorite you removed came back from another device’s local storage.
A union works when lists only grow. It cannot represent a deletion, because a missing item on one side looks the same as an item that side hasn’t seen yet. The more devices you use, the more places an old copy can survive.
The same commit also covers seeding in the same code. The first time the server has no list for a user, it makes sense to fill it from what is already in the browser. The commit limits seeding to that first time, and never after a network error.
Another finding was more about structure than behavior. The favorites list and the owned list each had their own copy of the sync hook. Two copies of the same logic means two places to fix, and two places where they can quietly drift apart.
How it was found
It was fixed in a code review round logged on 2026-07-02. Per the progress log, that round followed an earlier one that had focused on the UI, and this time it “took the code behind the UI”. The same round also moved the server from Express 4 to 5, added timeouts to all local LLM calls (LM Studio and Ollama: 120 seconds, model list 5 seconds), switched JSON files to atomic writes, and added a smoke test to CI. The sync bug was one item on that list.
Fixing the logic was half the work. The other half was proving the fix. We added a dedicated Playwright sync scenario with five checks. As the progress log describes it, the scenario deletes an item “on another device” and confirms it does not come back. The commit lists what was verified before it went in:
- smoke test against a booted server: 8 checks
- Playwright end-to-end suite: 14 checks
- sync scenario: 5 checks
- a production build
- Express 5.2.1 run locally
The counts come straight from the commit message. We have no timing figures or failure rates for the old behavior, so we won’t give any. We don’t know how often deletions actually came back for real users before the fix. We only know the logic allowed it and the new scenario shows it no longer does.
What changed
The fix changes who decides at startup.
- The server copy is the source of truth when it exists. At startup the app takes the server’s list as it is. It no longer merges in whatever happens to be in local storage.
- Seed from local only the first time. If the server has no list for this user yet, the local list is used to create one. That covers the move from local-only storage to synced storage without losing anything.
- Never seed after a network error. If the fetch fails, the app does not seed the server from local storage.
- One shared hook. The two duplicated hooks for favorites and owned items were replaced by a single shared sync hook. The rule now lives in one place.
Two related changes in the same commit make the server side safer too. Preference files are now written atomically (write to a temp file, then rename), so a crash in the middle of a write can no longer leave a half-written file. Reading also tolerates a UTF-8 byte order mark. Neither change fixes the union bug, but both protect the copy we now treat as the truth.
What we take from it
The union looked reasonable because it never loses data. That is also why it was wrong: a deletion is data, and a union throws it away. Every sync design has to decide which copy wins and when. Leaving that decision implicit gave us a list that could only grow.
The other lesson is about tests. The new scenario deletes an item on another device and checks that it does not come back. Without it, “fixed” would have meant “reads correctly”.
The rule
The server copy is the source of truth at startup. Seed from local storage only when the server has nothing yet, and never after a network error. Keep sync logic in one shared hook, not in copies. Any change to it has to pass the cross-device delete scenario.
Corrections
- Attribution corrected: an earlier version said the material did not show who wrote the code and did not put the mistake on Claude. Henning writes no code himself; the code was written by Claude.