Skip to content
Back to skills

Extract

BSecurity

Gets a design out of Figma and onto disk as JSON + assets, using the local "Design Twin" plugin (free Figma plan, no Figma API, no network egress). Use this whenever the user wants design data pulled, exported, refreshed or re-pulled from Figma — "pull the login screen", "export this frame", "get the design system", "get me the colors", "set up my theme from Figma", "grab the components from Figma" — and also whenever a build/codegen task needs files that aren't in design/export/ yet, or the ...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 20, 2026
designgoswiftbashreactnodeawsdebugginggitapi

Works with

  • claude code
  • terminal
  • cli
  • api
  • mcp

Security analysis

B85/100
  • highPerforms destructive filesystem operations

Pro scans all 3 files and shows the line behind each finding

Scanned October 20, 2026

npx -y skills add its-magdy/design-twin --skill extract --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Extract?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Extract
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/its-magdy-extract/badge)](https://www.skillsdirectory.com/skills/its-magdy-extract)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: extract
description: Gets a design out of Figma and onto disk as JSON + assets, using the local "Design Twin" plugin (free Figma plan, no Figma API, no network egress). Use this whenever the user wants design data pulled, exported, refreshed or re-pulled from Figma — "pull the login screen", "export this frame", "get the design system", "get me the colors", "set up my theme from Figma", "grab the components from Figma" — and also whenever a build/codegen task needs files that aren't in design/export/ yet, or the ones there are stale. This is the step before writing any code; once files land, hand off to build-screen. When a screen that is already built must catch up with a changed design, use sync-design — it snapshots before re-pulling.
argument-hint: "[what to pull: page, frame name, node id or Figma URL]"
---

# Extract a design from Figma

Get design data onto disk. Nothing here writes code — that's `build-screen`.

**Everything runs locally.** The Figma plugin's `allowedDomains` names only `ws://localhost`, so design data
never leaves the machine. There is no Figma API key and no cloud round-trip.

## Before anything: is the plugin running?

Every path needs the **Figma file open with the "Design Twin" plugin running**. None of this
is headless — the plugin *is* the reader. If the user hasn't started it: Figma desktop →
**Plugins → Development → Design Twin**.

Not imported yet? That's one-time setup — send them to `/designtwin:help`.

## References — load on demand

`<scripts>` in these files means `${CLAUDE_PLUGIN_ROOT}/scripts`, and `<plugin>` means `${CLAUDE_PLUGIN_ROOT}`
(Claude Code writes the real path here, not there).

| File | Load it when |
|------|--------------|
| `references/cross-check.md` | A pull produced a screen and a design system — what each cross-check finding means and what it changes. |
| `references/theme-tokens.md` | Turning the variables into a theme file — which input, what `tokens.js` writes, `--check`, `--lookup`, its warnings. |
| `${CLAUDE_PLUGIN_ROOT}/skills/help/references/troubleshooting.md` | Something failed — bridge offline, `EADDRINUSE`, empty library list, 401. |

## Where things land

`dtwin` writes to **`design/export/` and nowhere else**. Everything beside it under `design/` —
`target.json`, `codeconnect.local.json`, `plan/`, `audit/`, `verify/` — is decisions and evidence a
human or a later step owns, and no pull touches them.

That boundary is the point: `rm -rf design/export && re-pull` is the natural way to recover from a
bad export, and it does not take the hand-mapped component map with it. `design/README.md` (written by
`dtwin init`) says the same thing where someone deleting files will actually see it.

A project created before this split keeps its export directly in `design/`; every tool still finds
it, and `dtwin doctor` says which layout it found.

## Pick a path

Three ways to get data out: click the plugin's export buttons (manual), run the `dtwin` CLI (one-shot,
or held open as a daemon with `dtwin serve` for several pulls in a row), or go through a registered MCP server.
What each is, when to reach for it, and how a connection actually gets opened is the **help**
skill's job, not this one — use it if the user hasn't got a connection working yet.

**Default to manual export when the bridge isn't already working.** It needs zero setup and always
works; walking a user through CLI/token setup mid-task is worse than just clicking the buttons.

**Only one bridge can hold port 8787** at a time (CLI, daemon, and MCP all bind it) — if the MCP
server is already running, use its own export tools with `writeToDisk: true` instead of also trying
the CLI.

**Where `dtwin` comes from.** Check `which dtwin` first — it is on PATH when it was linked from a clone
(`npm link --workspace bridge`; the package is not on npm).
Only if it is missing are you inside a clone of the Design Twin repo, where every `dtwin` below is
`node bridge/src/figma-pull.ts`. If neither works, the CLI isn't installed: fall back to the plugin's own
export buttons, which need nothing.

## Discover, then scope — never open with a whole-file pull

A full pull on a real design file is enormous and most of it is irrelevant to the task. Spend two
cheap calls to find the one frame you actually need:

```
dtwin list                    # pages + their top-level LAYERS, with ids
dtwin list children <id>      # peek inside one node before committing
dtwin screenshot <id>         # look at a candidate before paying for a pull
dtwin pull --node <id>         # then deep-pull ONE screen (writes design/export/…)
dtwin pull --page <id>         # or a whole page
dtwin list libraries          # which design libraries this file draws on — SLOW, see below
```

Every command answers `--help` with its own page (`dtwin screenshot --help`, `dtwin whoami --help`,
`dtwin list --help`), and none of them does anything else while doing so. The output directory is
positional and defaults to `design/export` (`dtwin pull` writes there; never `--out`, and never bare
`design`, which makes a parallel export tree nothing else reads); the port is chosen only by `FIGMA_BRIDGE_PORT`
(8787/8788/8789). A direct run ends with `done in Xs — waited Ys for the plugin to connect · export Zs ·
write Ws`: a long "waited" is the plugin re-dialling a fresh bridge, not a slow export, and
`dtwin serve` (another terminal) keeps the plugin connected so later commands skip it. `dtwin screenshot`
prints `{id, name, type, w, h, scale, png:{w,h}, reference}` — size, render scale and PNG pixel size.
If a command stalls with "the plugin is connected but sent nothing", the plugin is busy with an earlier
export in that file: retry, or use `dtwin serve`. If a command hangs or times out, run
**`dtwin doctor`** first: it says whether the cause is the token, the port, the daemon or the plugin.

**`dtwin list` reports LAYERS, not screens.** Its top-level array carries SECTION, GROUP, INSTANCE,
TEXT and RECTANGLE alongside FRAME, and on a sectioned file the real screens are one level *deeper*,
inside SECTIONs 9,000-36,000px wide. The summary line breaks the count down by type and the JSON
carries `type` per row; when you see SECTIONs, `list children <section id>` is the next call, not a
pull.

**`dtwin list libraries` is the slowest read there is** — a few seconds up to ~15 s against half a
second for `list` (about 2 s on a file with no libraries enabled): it walks every instance in the file
and makes one Figma call per enabled library variable collection. It prints progress while
it works. Run it when you need to know which library owns a token, not as a warm-up.

**More than one Figma file open? Every command above needs `--client`.** The bridge routes per
connected plugin, so with two files connected it refuses rather than guessing:
"2 Figma files are connected to the bridge — say which one to use". That is not a failure — add
`--client <connId|part of the file name>` and re-run:

```
dtwin list clients                              # the address book: connId, file name, fileKey
dtwin list --client Marketing                   # …then scope every command to one file
dtwin pull --node <id> --client Marketing
```

A partial file name works and is what to reach for. The docs also offer `fileKey`, but on a
self-imported plugin (the free-plan setup this tool targets) `figma.fileKey` is gated and reported as
`null` — so that form addresses nothing here. `dtwin doctor` names each connected file and says when
disambiguation will be needed. MCP twin: a `client: "<id|name>"` argument on every plugin-reaching tool.

MCP twins, in the same order: `figma_status` (which files; pass a row's connId as `client`) →
`figma_list_pages {depth:1}` → `figma_list_children {nodeId:<page or section id>}` → (`figma_screenshot
{nodeId, scale:0.25, writeToDisk:true}` for look-alikes) → `figma_export_url {url:"<node id>",
writeToDisk:true}` for one screen. `figma_export_full({page:[id]})` is for a whole page only, and
`figma_list_libraries` stays out of the default sequence (it is the slow one).

Two narrower pulls worth knowing: `dtwin pull --design-system` gets tokens/styles/components with **no**
page walk and no assets (~15–40 s on a real file; progress shows only for clients that send a progress
token); `dtwin pull --as-library "<name>"` gets a library file's complete catalog (run it with the
*library* open, not the file consuming it).

**On a file that CONSUMES a library, `--design-system` returns that file's OWN tokens, styles and
components plus only the library variables something in the file references** (a page pull adds the ones
its nodes bind) — never the library's catalog. For that, open the LIBRARY file and
`dtwin pull --as-library "<name>"` (CLI only); theme from `libraries/<slug>/tokens.json`. The tell:
`remote: true` on the variables, and `hygiene.json`'s count line.

Pulling a design system to BUILD from (component states, variant looks)? Add `--variant-visuals` (MCP:
`variantVisuals: true`) — without it `get-component.js` / `design_get_component` return the prop list
only. A library's components need it on the `--as-library` pull (in the library file).

**Several frames with the same name?** Real files have them — one page held two frames with the same
name and the same size. `list children` gives every row a `childCount`, and rows that share name + size a
`title` (the first visible text) plus one warning per group — often enough. When the titles collide too
(a shared section heading), those rows also get `distinctTexts` (up to 3 texts that row shows and its twins
do not) — read them before pulling; when a listing is so large that its node-read budget runs out, the
remaining rows get none and one warning says `distinctTexts` was skipped. When `distinctTexts` is empty or
missing, don't take the first id: `dtwin screenshot <id>` renders ONE node to
`design/export/assets/<id>_ref.png` cheaply (no `serialize()`, no asset walk, well under a second
warm). Shoot each candidate, look, then pull the right one. At the default scale that PNG lands in
exactly the place a later `--node` pull of the same frame writes its own reference, so shooting first
costs nothing and leaves no duplicate. A cheap `--scale 0.25` thumbnail is written as
`<id>_shot@0.25x.png` instead, so it never stands in for the full-size reference. Ask the user if the renders don't settle it.

**A state you need may be a sibling frame, not a missing design.** Before reporting "there is no
populated/empty/error state", run `dtwin list children <the parent section>`. On the live file the
populated table sat beside the empty one, named for what it holds rather than for the screen beside it — and a
three-second sweep answered what would otherwise have been a blocking designer question.

**The full command + flag surface is `dtwin help`** (and `bridge/README.md` in a clone) — read it
rather than guessing; an unknown flag is refused with a suggestion. This skill owns the decision of
*which* pull to run; that reference owns *how*.

## Through the MCP instead

A server registered as `designtwin` in the project's `.mcp.json` (what `dtwin init --mcp` writes) names its
tools `mcp__designtwin__<tool>` (e.g. `mcp__designtwin__figma_list_pages`) — call it by that name if the
short one isn't found; under another registration the prefix differs.

**Pass `writeToDisk: true` on any export past a quick look.** Inline results are capped (25k tokens by
default). An export too large to return is written to disk on its own and the result's `note` says
so, but asking for it up front is cheaper than discovering it — and asset bytes are never returned
inline at all, so it's the only way to get `assets/`. The inline cap is also never above 48,000
characters. Every export result (written or inline) carries `sourceFile` — the Figma file it came from —
and `durationMs`; when an implicit spill replaces a file whose content differs (the `exportedAt`/`generatedAt` stamps aside), the old one is
kept one level and the `note` names it — a screen spill keeps ONLY `<screen>.json.prev` (`wrote.prev`;
its `.vars.json`/`.assets.json`, `pages/index.json` and `variables.json` are replaced with no copy), a
page/full/design-system spill a `<file>.prev` per changed JSON under `pages/`, `design-system/` and
`design-system.json` (`wrote.prevKept`). `figma_status` shows `lastScreenExport` and `lastWrite`.
With `writeToDisk`/`outDir`, a bad `outDir` is refused before the export runs.
A cancelled tool call cancels the read in the plugin.

## Check what landed before declaring success

Every export doc carries a **`manifest`**. Read all of it, not just the three fields that are
obviously about failure:

| key | what a non-zero value means |
|---|---|
| `nodes` | how much tree you got |
| `truncated`, `skipped` | the export did not finish the tree — treat as a failed pull |
| `assetsFailed` | a render failed and nothing replaced it |
| `warnings` | read every line |
| `assetsHidden` | hidden graphics not exported (`assetSkipped:"hidden"`). Normal, not a failure; the pull prints an info line, and the bridge reuses a visible twin's file where one matches. |
| `hiddenNodes` | nodes hidden themselves or under a hidden ancestor, kept in the tree (only the first carry `hidden:true`). Informational. |
| `assetsGeometry` | that many VISIBLE nodes took the **fallback** path: the render failed and their vector paths were inlined instead (any fallback warns and names them). `assetsFailed` stays 0, so the three obvious fields say "clean" while 13% of the nodes took a degraded route. Expect to hand-check those nodes. |
| `assetsSkippedInvisible` | vector nodes with nothing visible to render. Normal, and not a failure. |

Before scripting against these files (a walker, a count, a check), read **`design/export/SCHEMA.md`** —
the scripting quick keys (`text` not `characters`, `box.w` not `width`, `hidden` on ancestors, `scroll`,
`fixedChildren`…), written beside the data and the same text as the top of `ir-fields.md`. It is
rewritten only when its text changes, and a `SCHEMA.md` you wrote yourself is never touched.

**What lands depends on the pull**, so check for what your command actually produces:

- Every pull that writes to disk also leaves **`design/export/SCHEMA.md`** (the scripting quick keys).
- `--node <id>` / a selection → `design/export/pages/<Page>/<Screen>__<node-id>.json` (keys:
  `exportedAt`, `screen`, `page`, `pageId`, `nodeId`, `nodes[]`, `manifest`), plus three siblings:
  `…__<id>.vars.json` (the tokens THIS screen binds), `…__<id>.assets.json` (which assets it uses,
  with content hashes), and a row in `design/export/pages/index.json`. Also
  `design/export/variables.json` and `design/export/assets/`. **No `design/export/design-system/`** —
  that directory needs its own pull, and its absence here is normal, not a failed export.
- `--page <id>` / `--all-pages` → the same `pages/` tree, one file per top-level layer.
- `--design-system` → `design/export/design-system.json` (a slim pointer manifest — every entry under
  `files` resolves to something on disk) + `design/export/design-system/`: `tokens.json`,
  `components.local.json`, `components.library.json` (components this file *consumes* from
  elsewhere), `styles.{paint,text,effect,grid}.json`, `hygiene.json`. No page walk, no assets.

The filename carries the node id because frame names do not identify a frame: two `Popup`s on one
page are two different screens, and a name ending in a space sanitises to a trailing `_`. Read the
**root** `design/export/pages/index.json` rather than reconstructing filenames or opening a page
directory's own `index.json` one hop down — it carries a row per screen with the Figma layer `name`,
the visible `title` (the text on the frame's own title slot, NOT a sidebar/nav label that repeats on
every sibling screen), a `texts[]` fingerprint, `page`, `pageId`, `id` and the screen `file`. A
`--node`/selection row also carries `w`/`h` and, when written, `reference` and the paths to its
`.vars.json` / `.assets.json` siblings; a page-walk row (`--page`, `--all-pages`, a bare `dtwin pull`) carries `bytes` (and `nodes`)
but no `w`/`h`; its `reference` sits in the layer file, and it has no `.vars.json`/`.assets.json`.

**Resolving a screen by the name a user types (not the layer name Figma gave it):** the visible title
is very often NOT the Figma layer name — a layer named `people ` (trailing space) can be the frame
whose on-screen `<h1>` reads "Members". Every skill resolves with the one shared script,
`node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-screen.js" <exportDir> "<name-or-id>" design/plan`. The query may also
be a `<Layer>__<id>` basename or a path to the screen's `.json` (or `.vars.json`); a folder that is not an export
root (no `pages/index.json`) prints `not an export root` and the root to pass instead. How it decides:

- A node id (e.g. `7314:87192`) is tried alone and, if it matches, wins outright — an id is unique.
- Otherwise THREE exact checks — layer name, indexed `title`, a plan's `screenName`/`route` (each trimmed,
  case-insensitive) — form ONE pool: any row matching any of them joins it. Exactly one row resolves — with a
  `stem` line, the `<Layer>__<id>` a NEW plan and the verify set take (the name a verify set of that node already
  has, so a frame renamed in Figma keeps it; an existing plan keeps its own name); more than one
  stops and prints every candidate (name, id, size, node count, `dtwin screenshot <id>`, and which field each
  matched on). The pool is what keeps a layer-name hit from hiding a second row titled the same — the empty-state
  sibling of "Members", layered `members`, is exactly this case.
- With an empty pool (and no node id the query carries, as a `<Layer>__<id>` name or a `?node-id=` URL does), the
  same three fields are compared ignoring spaces, `-`, `_` and case ("OrderList" for "Order List"), then a looser
  case-insensitive substring search over `name`/`title`/`texts[]` runs. Their hits are only a candidate list (exit
  1, the whitespace-insensitive ones first), even a single one: two names that differ only by spacing may be two
  screens, so print the candidate(s) and ask the user to confirm by node id before building, auditing or verifying it.
- When not one index row carries a `title`, it says so: the export predates title indexing, and a re-pull of the
  screen (`dtwin pull --node <id>`) fixes it. A screen never pulled cannot be named by its title yet —
  `dtwin list`/`dtwin screenshot` show only Figma layer names — so the FIRST pull of a screen is by node id (from
  `dtwin list` or a screenshot), and title lookup works from the second command on.

`--design-system` counts and `dtwin list libraries` counts legitimately differ: the export includes
variables this file merely *references* from a published library, flagged `remote: true`, and
`hygiene.json`'s first line says how many. Both numbers are right; they are answering different
questions.

**`design/export/variables.json` accumulates — it is not per-pull state.** Each single-screen pull
merges its slice in, keyed on each variable's Figma key, so pulling screen B keeps screen
A's tokens. The raw per-pull slice is also kept verbatim as the screen's `.vars.json` (every variable
in the collections that screen references — more than the ones its nodes actually bind). Names are
not unique across the union: two different variables (two keys) can both be called `Space 4` with
different values. Such a pair, and a variable two pulls resolve differently, is recorded under
`_conflicts` and as a `CONFLICT` line in `hygiene` — read those before generating a theme.
`variables.json` holds only variables bound by pulled nodes — look up library colours with `--lookup` on
the library catalog (see below).

**Check the reference screenshot landed.** Every export renders one PNG per top-level frame and
points at it from a `reference` field holding a path relative to the export dir
(`assets/<id>_ref.png`). Where that field sits depends on the export: a single-screen pull puts it on
each node (`nodes[0].reference`), a page-walk layer file puts it at the root beside `tree`.
`build-screen` validates against it, so confirm the file exists. Only if `reference` is absent (the
manifest `warnings` will say so) ask the user to export a PNG by hand (Figma right-click → Export).

**Assets are named after their Figma layer and deduped by content.** `icons/linear/arrow-down` lands
as `arrow-down.svg`, not as a node-id path, and the same artwork reached through several instance
paths becomes one file with every node id recorded on it. Two different icons sharing a leaf name are
told apart by a short content hash, never by overwriting. Image *fills* are the one exception: they
carry a `hash` rather than a path, and the byte lands at `assets/img_<hash>.<ext>` —
`…__<id>.assets.json` indexes all of it, so read that instead of deriving the convention.

**A pull warns about assets too heavy to inline.** A flattened illustration exports as one SVG with
thousands of paths — the live run hit 2.47 MB, which inlined into a 2.71 MB JS bundle until it was
moved to a URL import. Import those by URL, or ask the designer to re-export as a PNG; leave the artwork as
exported rather than redrawing or simplifying it.

## Is this screen even from that design system?

Pulling the design system and then pulling a screen does **not** mean the two are related — both may be copies of
one original (names and values line up while every key differs), or the screen may consume a library you never
exported. One command answers it:

```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/cross-check.js" design/export/pages/<Page>/<Screen>__<id>.json \
  --design-system design/export/design-system
```

Run it after any pull that produced both, and report its blockers and every warning that carries a `confirm`
question (asked, with the default the build takes) before offering to build. What each finding means and what to
do about it — token collisions, `foreign-token-library` and its offline name-map counts, `catalog-covers-nothing`,
`catalog-rekeyed` proposals the user confirms before `map-bootstrap.js --from-proposals` stubs them — is in
`references/cross-check.md`; read it before you report. `/designtwin:audit-design` runs the same pass as part of
its report; this is the standalone form for when you have only just pulled.

## Tokens → a theme file the app can import

"Get me the colors" is not finished when the raw variables land — nothing in the app can import those. Turn them
into the stack's own theme file once, instead of re-mapping values on every screen:

```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/tokens.js" <tokens.json> design/export/   # DTCG json + tokens.css + a tokens/ set dir
node "${CLAUDE_PLUGIN_ROOT}/scripts/tokens.js" <tokens.json> <css dir> --web tailwind
node "${CLAUDE_PLUGIN_ROOT}/scripts/tokens.js" <tokens.json> <theme dir> --native swiftui   # or compose | flutter | react-native
```

Which `tokens.json` to feed it changes the values, not just the coverage — read `references/theme-tokens.md` before
generating: the input order (a pulled library's catalog, the defining file's design system, the screen's own
`.vars.json`, the merged union last), what it writes, `--check` and `--lookup`, and its warnings.

## Then hand off

Report briefly: what landed, anything the manifest flagged, whether the PNG exists, and any
cross-check blocker or `confirm` question. Then **`/designtwin:audit-design <screen>`** to check the design is buildable
(missing states, contrast, touch targets, designer questions) — its `prototype-target-not-exported` names the dialogs and overlays the screen opens that nobody pulled: pull those too — and
**`/designtwin:build-screen <screen>`** to build it.

If something failed — bridge offline, `EADDRINUSE`, empty library list, 401 — the symptom→fix list is
the **help** skill's troubleshooting reference (table above). Load it rather than debugging from memory.

Files in this skill

  • SKILL.md23.6 KB
  • references/cross-check.md6.6 KB
  • references/theme-tokens.md5.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.