--- name: design-sync description: Push a React design system to claude.ai/design. This runs a converter that bundles the real component code (from Storybook or a bare package) and uploads it. Use when the user runs /design-sync or says "sync my design system to Claude Design". --- # Sync a design system to claude.ai/design ## What this is for **Claude Design** (claude.ai/design) is Claude's design tool: users prompt a design agent and it builds working UI — screens, flows, prototypes — rendered live in the browser from real React code. Out of the box it designs with generic components. This skill changes that: it converts the user's design-system repo into the format Claude Design consumes and uploads it, so from then on **the design agent builds with the customer's actual components** — every design it produces is on-brand, made of their real parts, and maps 1:1 onto code their engineers can ship. That framing should drive every judgment call in this skill, because each uploaded artifact is an input to that agent (or to the humans steering it): | Uploaded artifact | Consumed by | For | |---|---|---| | `_ds_bundle.js` + `_vendor/` | the design agent's runtime | every design it produces renders these real compiled components from `window..*` | | `styles.css`, `fonts/`, `tokens/`, `_ds_bundle.css` | every rendered design | the look — tokens, fonts, and component styles, all reachable from `styles.css`'s `@import` closure (designs receive only that closure) | | `.d.ts` (`Props`) | the design agent | the API contract it codes against | | `.prompt.md` | the design agent | its usage reference — how to compose the component, with examples | | `.html` preview card | humans in the component picker | how they find components and trust the sync | | `_ds_sync.json` | future syncs | the sync anchor — content hashes that let a re-sync (any machine) skip re-verifying unchanged components AND compute exactly what to upload/delete | This is why fidelity is the whole game: a component that renders wrong here renders wrong in **every design the agent ever builds with it**, and a wrong `.d.ts` or misleading `.prompt.md` makes the agent misuse the API everywhere. The verification loops in the sub-skills exist because of this — they are not bureaucracy. The converter builds all of the above deterministically from the repo's own `dist/`. With a Storybook, previews come from the repo's stories and are verified against its own storybook render (kept as a local reference, never uploaded). Without one, every component still ships fully functional, and rich previews are authored from the repo's own usage examples for the components the user scopes in, graded on an absolute rubric. **Core principle: ship what the customer already built** — the bundle is their compiled `dist/`, never a reimplementation. You have a `DesignSync` tool that reads and writes the user's claude.ai/design projects. ## 0. First sync? Set expectations before any work A completed sync always leaves `.design-sync/config.json` holding both a `projectId` and a `pkg`. If both are present, this is a re-sync — skip this section (§2 covers honoring prior state). (If `design-sync.config.json` exists instead — the config's old name and location — move it: `mkdir -p .design-sync && mv -n design-sync.config.json .design-sync/config.json`, commit the move, then apply the same test.) Anything less — no config at all, or a partial one left by a run that never finished — gets first-time treatment: tell the user up front, before doing anything else: - No completed sync was found — this is a first-time import. - This skill attempts a **high-fidelity** import of their design system: by default that means iterating on the build and visually verifying the quality of every component preview, which can take **up to a few hours** on a large repo. - They can interrupt at any time — a message mid-run to check progress or redirect the effort is welcome and won't break anything. - A first-time import goes into a **new Claude Design project created for it** (§1). Everything that needs their approval happens **near the start** — creating that project, and one approval that covers this run's uploads into it. After that, **verified components appear in the project as the run progresses**: they can open the project at any time and watch it fill in, and nothing waits on their approval at the end. - The run records config and notes as it goes, so future syncs are faster and mostly deterministic. (If §1 routes this run into an existing project — the user re-adopting one, or a `projectId` left pinned by an aborted run — parts of this won't apply; scale the expectations to what §1 routes them to.) Then confirm they want to proceed — this process can use a significant number of tokens (`AskUserQuestion`: proceed with the full high-fidelity sync, or adjust scope first). If their request already acknowledged the time/cost, note that and continue without re-asking. ## 1. Pick the target project If `DesignSync` isn't already in your tool list, load it via `ToolSearch(query: "select:DesignSync")` first. A target gets picked one of three ways, in precedence order: - **Pinned**: `.design-sync/config.json` has a `projectId` → that's the target. `DesignSync(get_project)` to confirm it still exists and is `PROJECT_TYPE_DESIGN_SYSTEM`, mention which project you're syncing to, and re-ask only if it's gone or the user redirects. - **Fresh — the first-time default**: no pin → **create a new project**. A fresh project is the only target whose entire contents this run owns; that ownership is what makes the incremental upload (§3) safe to approve in one shot, and it's why existing projects are never offered here — pouring a first import into a project that already has files would show a half-imported mix to anyone using it, with no sync anchor to tell its files apart from this run's. Use `DesignSync(list_projects)` to pick a NON-colliding name (a duplicate gets rejected and costs a round-trip), confirm the name via `AskUserQuestion`, and only then call `DesignSync(create_project)` — it raises its own permission prompt, and an unconfirmed creation can stall an unattended session. If that prompt is denied, stop and ask the user what to do differently; never retry unasked, never continue without a target. One salvage case: a project evidently left by a prior aborted run of this repo (it has the name this skill would propose — `list_files` it to confirm it's actually empty, since `list_projects` shows no file counts) may be offered for reuse instead of creating another, or noted as safe to delete. - **Re-adopted — on the user's explicit ask only**: the user names an existing project (by name or UUID; typically re-adopting the project a previous sync uploaded to, after the config was lost). `DesignSync(get_project)`, check `type` is `PROJECT_TYPE_DESIGN_SYSTEM`, then warn them in plain language (no tool jargon) that syncing can overwrite or delete files already in it — e.g. "Heads up: syncing into that existing project means I may replace or remove files it already contains so it ends up matching this repo. If anything in there isn't from this repo, it could be lost — want me to continue, or create a fresh project instead?" — and proceed only on their confirmation. This explicit ask is the ONLY way an unpinned run ends up in a pre-existing project. **Record the pin at settlement.** The moment the target is settled — created, reused, or re-adopted — **record its `projectId` in `.design-sync/config.json`**, before anything uploads. This is the skill's one recording rule: a death at any later point leaves a pinned config, so the retry repairs the SAME project through the atomic path instead of creating a duplicate and orphaning the original. (The post-upload record step in the sub-skills' atomic sections is just the backstop for this rule.) **Route the upload path.** A `projectId` pinned **before this run started** always takes the **atomic path** (the sub-skill's upload section) — even when its project turns out empty; a bulk re-upload is fine there, and one rule beats a special case. Otherwise the remote decides, via a prompt-free `DesignSync(list_files)` on the target: - **Empty** (the normal case — this run just created it) → **incremental path** (§3): one upfront approval, then verified components upload as the run progresses. - **Non-empty** (a re-adopted project) → **atomic path**: it may be in active use, so it updates in one pass at the end of the run, after everything is verified. The router decides only the **upload** path. **Verification** scope is the anchor's job: a project with `_ds_sync.json` lets the re-sync driver skip unchanged components; no anchor means everything gets verified, whichever upload path applies. ## 2. Explore, then write config The workflow is **explore the repo → write `.design-sync/config.json` (§1's pin has already created the directory and the file — read it and add to it, never dropping `projectId`; `mkdir -p .design-sync` stays as a harmless safety net for legacy states) → run the converter deterministically from it**. The converter's discovery is heuristic-based; each heuristic has a config override (after the sub-skill stages the scripts: `grep -r ASSUMPTION .ds-sync/*.mjs .ds-sync/lib/*.mjs` lists them) so repos that don't match the defaults write config, not code. Edit `lib/*.mjs` only as a last resort (see the sub-skill's escape-hatch section: storybook §5, package §Troubleshooting). **The upload format is the contract; the converter is the deterministic path to it, not the only path.** What the app consumes is fully specified by the output layout: `_ds_bundle.js` + `@ds-bundle` header, `styles.css`, `components///{.html,.jsx,.d.ts,.prompt.md}` with the `@dsCard` first line, `_preview/`, `_vendor/`, `fonts/`, `_ds_sync.json` (see the sub-skill's layout and upload sections). An off-script layout should also produce `_ds_sync.json` when it can. For the package shape, `lib/sync-hashes.mjs` gives `styleShaFor`/`renderHashFor`/`sourceKeyFor`; the envelope is `{shape, styleSha, renderHashes, sourceKeys, keyRecipe, scriptsSha, sourceHashes, auxSha, bundleSha12}` (see the sidecar block in `package-build.mjs` — `sourceHashes` itself comes from `stampHeader` in `lib/bundle.mjs`; `sourceKeys` may be omitted, which just means changed artifacts re-verify). The storybook shape's recipe needs story facts an off-script generator may not have; omitting the sidecar is then the honest choice — the next sync simply has no anchor and re-verifies everything, which is correct. One invariant that's easy to miss when producing the layout by hand: rendered designs receive only `styles.css`'s transitive `@import` closure. Any real component CSS (`_ds_bundle.css`) must be `@import`ed from `styles.css` — a card linking it directly proves nothing about designs. For a repo genuinely outside the converter's envelope (non-esbuild-bundlable builds, exotic toolchains), produce the layout by whatever means the repo allows. The gates don't move: `package-validate.mjs` must exit clean, and every story must be graded before upload — from true screenshot pairs in the storybook shape, on the absolute rubric in the package shape. Off-script generation is legitimate; off-script *verification* is not. **State from prior runs.** If `.design-sync/config.json` or `.design-sync/NOTES.md` already exist, Read both first and honor what's there — they hold corrections from earlier syncs. **Whenever the user tells you about an issue mid-run** (a path, a build flag, a component to skip, a package-manager quirk), persist it immediately so the next sync doesn't need telling again: a value that maps to a `cfg.*` field goes into `.design-sync/config.json`; anything else goes as a bullet in `.design-sync/NOTES.md`. Both get committed at the end (the sub-skill says when). 1. **Faithful install with the repo's own package manager.** Use the repo's pinned node version (`.nvmrc` / `engines.node`), then detect via lockfile: `yarn.lock` → `yarn install --immutable`; `pnpm-lock.yaml` → `pnpm i --frozen-lockfile`; `bun.lockb`/`bun.lock` → `bun install --frozen-lockfile`; `package-lock.json` → `npm ci`. 2. **Determine the source shape.** If `.design-sync/config.json` already exists and has a `"shape"` field, use that. Otherwise `Glob` for `**/.storybook/main.*` and `**/storybook/main.*` (some repos drop the dot; exclude `node_modules`) — monorepo DSes keep it in a subpackage, so never assume it's at repo root: - Any match → `shape = 'storybook'`. The match's grandparent is the package to run from. Found several → `AskUserQuestion` which one is the design system's; that dir becomes `storybookConfigDir`. **Do not fall back to package just because `.storybook` isn't at repo root.** - Found `*.stories.*` files but no `.storybook/` dir in the target → `AskUserQuestion`: "Found story files but no `.storybook/` here — is there a Storybook config elsewhere in this repo (e.g. `apps/storybook/.storybook` in a monorepo)?" If they point at one → `shape = 'storybook'`, record that path as `storybookConfigDir`. If they say no → `shape = 'package'`. - No `.storybook/` and no `*.stories.*` → `AskUserQuestion` whether a Storybook exists at all. If they point at one, record it as `storybookConfigDir` and `shape = 'storybook'`. If no, `shape = 'package'`. Then `Read` `/storybook/SKILL.md` or `/non-storybook/SKILL.md` and follow it from there (the storybook one points back into the package one's shared tables where they overlap). Record `"shape"` (and `"storybookConfigDir"` when set) in `.design-sync/config.json` when you write it so re-sync skips detection. Both shapes run `/package-build.mjs` as the converter entry and `/resync.mjs` as the single re-sync driver (build → diff → validate → scoped capture, one verdict JSON); shared adapters live at `/lib/`, and `/storybook/` holds the storybook-only harness (`compare.mjs` — preview-vs-storybook matching; `probe.mjs` — provider inference fallback). ## 3. The incremental upload sequence (first syncs into an empty project) On the incremental path (§1), the user approves the upload once, early, and then watches verified components appear in their project while the run is still going — instead of waiting hours for one bulk upload at the end. This section is the shared mechanics; the sub-skill says **when** each step fires (its own build and verification gates, marked "incremental path" there). The sub-skill upload section's mechanics apply to every write here too: ≤256 files per `write_files` call and smaller chunks for binary-heavy dirs, upload hygiene, and the what-stays-local list. ### Open the upload channel — at the sub-skill's first-clean-build gate 1. **Explain the approval in plain language first.** Before asking, tell the user what they're about to approve, with no tool jargon (no "plan", "glob", or tool-method names): e.g. *"I'll ask for one approval now that covers uploading everything this run produces into the new project — and cleaning up any files a later rebuild drops. You won't be prompted again; components will appear in the project as they're verified."* The approval dialog shows a structured path list on its own; this message is what makes that dialog make sense to someone who's never synced before. 2. `DesignSync(finalize_plan)` with `localDir: "./ds-bundle"`, `writes: ["components/**", "tokens/**", "fonts/**", "_vendor/**", "_preview/**", "guidelines/**", "_ds_bundle.js", "_ds_bundle.css", "styles.css", "README.md", "_ds_sync.json", "_ds_needs_recompile"]`, and `deletes: ["components/**", "tokens/**", "fonts/**", "_vendor/**", "_preview/**", "guidelines/**"]`. The delete globs are what make the end-of-run reconciliation below prompt-free — and they're consent-trivial here: the project started empty, so anything deletable is something this same run uploaded. The returned `planId` serves the whole run (it lives for the session). Lost mid-run to a context reset → `finalize_plan` again, one fresh approval, before uploading anything more. A whole-session death doesn't resume this path at all: the retry arrives pinned (§1) and correctly goes atomic — expected, not a bug to work around. 3. **If the approval is denied, stop and ask — never continue silently, never re-prompt unasked.** Say in plain language what was denied and what it covered ("the one-time approval for uploading this run's output into the new project"), then offer: try the approval again; target a different project; or finish the build and verification locally with no upload. Local-only → the run proceeds normally except nothing uploads, and the end-of-run report hands over both the `ds-bundle/` path and the project's URL (`https://claude.ai/design/p/` — the pin is already recorded, so a later sync finds this project rather than orphaning it). A different project → it goes through §1's re-adoption ask and the router like any other explicit choice, pin included: non-empty → atomic path, this plan abandoned; empty → resume here with a fresh approval. ### Push each verified batch Nothing uploads until the first batch of components passes the sub-skill's done-bar. **The first push carries the shared base files together with that first batch**: `_ds_bundle.js`, `_ds_bundle.css`, `styles.css`, `README.md`, `_vendor/**`, `tokens/**`, `fonts/**`, `guidelines/**`, plus the batch's `components///` dirs and `_preview/.*` files. Two reasons they travel together: the first thing the user sees in the project is real components, not an empty shell that claims something was uploaded — and by first-batch time the shared files have earned their place, because grading those components exercised the very same bundle, CSS, and fonts. This first push is the project's first content and its largest, so it takes the full fence: sentinel first (`write_files` `_ds_needs_recompile` — it fences the app's manifest/copy machinery against a half-uploaded state), then the files, then the sentinel re-write (every push on this path ends by re-writing the sentinel — that's what makes the app refresh its view of the project next time it's opened). Output the project URL prominently with this push — `https://claude.ai/design/p/` — it's the moment the project first has something to see. Every later batch that passes the done-bar: `write_files` its `components///` dirs and `_preview/.*` files, then re-write the sentinel — the new cards appear next time the user opens or refreshes the project. When you report batch progress, include the project URL so the new cards are one click away. If a full rebuild has run since the last push (a global config fix landed), include the shared base files again: the fix rewrote the bundle/CSS/fonts locally, and without re-pushing them every component verified after it renders against stale remote versions until close-out. They're in the approved plan and idempotent, so the re-push costs nothing. Later batch pushes need no leading fence — they're short and always end re-armed, so the unfenced window is negligible (the first push above and the long close-out below are the ones that fence first). And batches are progressive visibility, not the correctness mechanism: the close-out guarantees the final state, so don't agonize over batch composition — a component pushed early then reworked later simply gets re-pushed. ### Close out — after the sub-skill's final gate 1. **Sentinel first, then full content writes.** Re-write `_ds_needs_recompile` before anything else — the app clears the sentinel whenever the user opens the project (which this path invites mid-run), and the close-out is the longest write+delete stretch, so re-fencing here is what keeps a half-applied state from ever being consumed. Then everything in the plan's writes EXCEPT `_ds_sync.json`, chunked. Re-uploading unchanged files is idempotent and cheap; this pass covers anything the batches missed and anything the final rebuild changed, so the project ends up exactly matching the final verified build no matter how the batches went. 2. **Reconciliation deletes — mandatory, not conditional.** `DesignSync(list_files)` the project and `delete_files` every remote path under `components/`, `_preview/`, `tokens/`, `fonts/`, `_vendor/`, `guidelines/` that the final `ds-bundle/` does not contain (the plan's delete globs cover them — no new prompt). Why this pass exists: a component uploaded by an earlier batch and then dropped, renamed, or regrouped later in the run is invisible to every future re-sync diff — anchor-based diffs only see what the anchor records — so this is the only moment it can ever be cleaned up; skip it and the orphan is permanent. The deletes also retire the orphan's card: the app rebuilds its component index from the currently-uploaded files, so the card disappears once the sentinel is re-armed (next step) and the project is opened. 3. **Sentinel re-arm, then `_ds_sync.json` absolutely last**, in its own `write_files` call — same rule, same reason as the atomic path: the anchor must only ever vouch for a fully-applied state, and it goes after the deletes so a failed delete can't leave remote files the anchor no longer sees. Then output the project URL — `https://claude.ai/design/p/` — with the final summary. A mid-run abort anywhere on this path (user stops the run, session dies) leaves the project **un-anchored** — the documented safe state: the next sync re-verifies everything and re-uploads, nothing silently rots. And as in the sub-skill upload sections, any write/delete failure that retries don't clear means **STOP** — no sentinel re-arm, no `_ds_sync.json`.