The Diff That Resyncs Claude Design With a Repo That Moved
One diff reconciles a canvas that drifted from a repo your teammates kept changing, so the screen you present is the product that actually ships. Claude design out of sync with repo has no merge button: there are two stores with two jobs, one rule that makes the reconciliation correct, and a way to be told next time instead of finding out.
>This reconciles one divergence. Claude Design Sync carries the same project from a first clean loop to a live app that keeps agreeing with its design.

Claude Design Sync: Ship the Real App
The Two-Way Claude Code System for Shipping Production Apps Without Drift
Hello builders,
One diff resyncs a drifted Claude Design canvas with a repo that moved, so the screen you present is the product that actually ships. Somebody on your team is going to merge a pull request this week, and the design you have been working on will quietly stop describing the product. Claude design out of sync with repo is what that feels like from your side, and the fix is not a merge button, because there is not one.
It happened to me on a Thursday, and the work was good. A teammate renamed a prop that had always been badly named, split an overloaded component into two, and updated every call site. The build was green, the team was happier, and the canvas I hadn’t opened since Tuesday was describing an application that did not exist. Nobody did anything wrong, which is the entire problem, and it’s why a standup won’t fix this. Getting the mental model right is most of the fix, so start there.
Two stores, two jobs
Drift feels unmanageable because it looks like two copies of one thing, both moving, with no way to say which one is right. That’s the wrong model. There are not two copies. There are two stores with two different jobs:
“The repo carries the sync’s inputs (config, owned previews, NOTES.md); the uploaded project carries the anchor (
_ds_sync.json).”
That sentence is about authority. Our repository holds the inputs, and that is the truth about what the design system is. The uploaded project holds the anchor, and an anchor is content hashes recording what was last agreed, as of the last successful sync.
So when that change lands, nothing is in conflict the way it feels. The repo moved forward honestly to a new true state. The anchor still records the old one, just as honestly, because nobody has told it otherwise. The disagreement between them is a diff, and a diff is a thing we can compute, which is exactly why this is solvable and a design-file-versus-codebase standoff is not.
The reconciliation runs through a driver script, and it is idempotent, so if a stage fails we fix that stage and run the identical command again:
node .ds-sync/resync.mjs --config .design-sync/config.json --node-modules <nm> \
--out ./ds-bundle --remote .design-sync/.cache/remote-sync.json
Changed is not one question
Here is the lesson. It’s the one that decides whether the reconciliation was correct or quietly wrong.

Diverge the project on purpose the way a teammate diverges it by accident. Change packages/harbor/Button.d.ts and nothing else, tightening exactly the kind of type a careful teammate tightens:
// packages/harbor/Button.d.ts, before
export interface ButtonProps {
variant?: string
}
// and after
export interface ButtonProps {
variant?: 'primary' | 'ghost'
}
The rendered button is pixel-identical. The contract underneath it is not. Now run the reconciliation and read what it decides, because the diff sorts your components two different ways at once.
The verification partition asks what needs looking at again. Button renders identically, so it files under unchanged and skips grading entirely. The upload partition asks what files the project is missing, and it works off source hashes rather than renders, so Button.d.ts changed, its hash changed, and it ships. Card, Chip and Input are unchanged in both columns: nothing to re-grade and nothing to send.
And now the rule, stated as a flat prohibition: never scope uploads by the verification partition. Scope that way and we skip Button entirely, because it looks the same. The uploaded project keeps the old contract, the design agent goes on coding against variant?: string, and it cheerfully generates <Button variant="cta"> forever, which renders with no variant styling and no error. A change with no visual footprint still has to ship, because the contract is the thing that moved.
One thing that lets us stop a reconciliation without fear: the anchor is written last, in its own step, and only ever vouches for a fully-applied state. A mid-run abort leaves the project un-anchored, which is the documented safe state, and the next sync re-verifies and re-uploads everything. Nothing rots in silence, so Ctrl-C is a real option.
Catching it next time
Everything above is recovery. The version of this that costs a client is the drift nobody noticed, so the real question is how we find out we need to reconcile.
The token gate doesn’t do this, and the reason it matters is worth a sentence. A token gate answers “is this value off-system?” It’s blind to “does this look different than it did?” A layout can collapse and a card can lose its padding using perfectly legal tokens, and the gate passes it, because every value in the wreckage is approved.
The instrument for changed is visual regression testing, and Playwright ships it in about three lines:
// tests/design.spec.ts — point it at a page you actually designed
import { test, expect } from '@playwright/test';
test('the page matches its baseline', async ({ page }) => {
await page.goto(`${process.env.BASE_URL}/the-page-you-redesigned`);
await expect(page).toHaveScreenshot();
});
The honest cost is environment pinning, and the vendor says so itself:
“Browser rendering can vary based on the host OS, version, settings, hardware, power source (battery vs. power adapter), headless mode, and other factors. For consistent screenshots, run tests in the same environment where the baseline screenshots were generated.”
That warning produces a first failure nobody expects. Baselines are stored per platform and per browser, so a test that only ever ran on a Mac reports a missing baseline in Linux CI, looking for -chromium-linux.png when only -chromium-darwin.png exists. (Visual comparisons, Playwright docs)
Now go build something this weekend!
John Cook