| name | figma-kiwi-protocol-decode |
| description | Decode Figma's binary Kiwi wire protocol to extract scenegraph, SVGs, and CSS from WebSocket frames, and write mutations back to live files. |
| triggers | ["extract data from a Figma file","decode Figma scenegraph without REST API","get SVG paths from Figma vectors","write changes to Figma files programmatically","bypass Figma API rate limits","capture Figma WebSocket frames","clone Figma components via protocol","extract CSS from Figma nodes"] |
Figma Kiwi Protocol Skill
Skill by ara.so — Design Skills collection.
What This Does
Figma uses Kiwi (binary serialization by Evan Wallace) over WebSocket for real-time sync. This library decodes those binary frames to extract the full scenegraph, SVG vectors, and CSS — then writes mutations back to live files. No REST API rate limits, no paid plan required.
Read path: Intercept WebSocket frames via Chrome DevTools Protocol, decode binary data.
Write path: Open standalone Node WebSocket to Figma's multiplayer endpoint, apply mutations that propagate live.
Ships as lib, CLI, MCP server, and Claude Code plugin.
Installation
npm install figma-kiwi-protocol
CLI Quick Start
Read: Capture and Decode
chrome --remote-debugging-port=9222
export CDP_WS_URL="ws://localhost:9222/devtools/browser/<id>"
export FIGMA_TOKEN="figd_..."
export FIGMA_FILE_KEY="abc123def"
npx figma-kiwi-protocol capture-all-pages
npx figma-kiwi-protocol decode
npx figma-kiwi-protocol extract-svgs
npx figma-kiwi-protocol decode-frames
npx figma-kiwi-protocol comments $FIGMA_FILE_KEY --threads --flat
Write: Mutations and Cloning
npx figma-kiwi-protocol recon-handshake
npx figma-kiwi-protocol write rename 2004:16177 "NEW_NAME"
npx figma-kiwi-protocol write enable-al 2010:16169 \
--direction HORIZONTAL --hug both --padding 16 --spacing 8
npx figma-kiwi-protocol clone 41:1923 --name-suffix " (clone)"
npx figma-kiwi-protocol write batch ./changes.json --dry-run
Library Usage
Read: Decode Binary Data
import {
commandsBlobToPath,
vectorNetworkBlobToPath,
extractSvgs,
extractCSSFromKiwi,
isFigWireFrame,
extractCompressedSchema,
mergePages,
buildTree
} from 'figma-kiwi-protocol';
const blobBytes = Buffer.from(node.commandsBlob);
const svgPath = commandsBlobToPath(blobBytes);
const vectorBlob = Buffer.from(node.vectorNetworkBlob);
const editablePath = vectorNetworkBlobToPath(vectorBlob);
const nodeChange = scenegraph.nodeChanges.find(n => n.guid === '41:1923');
const css = extractCSSFromKiwi(nodeChange);
const frameBytes = fs.readFileSync('/tmp/figma_kiwi/frame_000.bin');
if (isFigWireFrame(frameBytes)) {
const compressedSchema = extractCompressedSchema(frameBytes);
}
const page1 = .(fs.());
page2 = .(fs.());
merged = ([page1, page2]);
tree = (merged.);
Write: Mutations and Session Management
import { FigmaSession } from 'figma-kiwi-protocol/session';
const session = await FigmaSession.connect();
await session.rename(
{ sessionID: 2004, localID: 16177 },
'ButtonPrimary'
);
await session.setStackMode(
{ sessionID: 2010, localID: 16169 },
'HORIZONTAL'
);
await session.setPadding(
{ sessionID: 2010, localID: 16169 },
{ all: 16 }
);
await session.setSpacing(
{ sessionID: 2010, localID: 16169 },
8
);
await session.setSizing(
{ sessionID: 2010, localID: 16169 },
'HORIZONTAL',
'HUG'
);
await session.({
: [
{
: ,
: ,
: { : }
}
],
: []
});
session.();
Clone: Deep Copy Components
import { cloneSubtree } from 'figma-kiwi-protocol/clone';
import { FigmaSession } from 'figma-kiwi-protocol/session';
import fs from 'fs';
const scenegraph = JSON.parse(
fs.readFileSync('/tmp/figma_kiwi/scenegraph.json')
);
const session = await FigmaSession.connect();
const { nodeChanges, blobs } = cloneSubtree({
scenegraph,
sourceGuid: '41:1923',
sessionID: session.sessionID,
nameSuffix: ' (Clone)'
});
await session.mutate({ nodeChanges, blobs });
await session.close();
Builder: Author Nodes from Scratch
import { FigmaBuilder } from 'figma-kiwi-protocol/builder';
import { FigmaSession } from 'figma-kiwi-protocol/session';
const session = await FigmaSession.connect();
const b = new FigmaBuilder({ sessionID: session.sessionID });
const card = b.frame({
name: 'Card',
stackMode: 'VERTICAL',
padding: { all: 16 },
spacing: 12,
size: { x: 320, y: 200 }
});
b.rectangle({
parent: card,
size: { x: 288, y: 120 },
fills: [{ type: 'SOLID', color: { r: 0.1, g: 0.1, b: 0.1, a: 1 } }]
});
b.({
: card,
: ,
: ,
: ,
: { : , : }
});
nodeChanges = b.();
session.({ nodeChanges });
session.();
Configuration
Environment Variables
CDP_WS_URL="ws://localhost:9222/devtools/browser/<id>"
FIGMA_TOKEN="figd_..."
FIGMA_FILE_KEY="abc123def"
FIGMA_HANDSHAKE_PATH="/custom/path/handshake.json"
FIGMA_KIWI_DIR="/tmp/figma_kiwi"
MCP Server Setup
Add to .mcp.json or ~/.claude/settings.json:
{
"mcpServers": {
"figma": {
"command": "node",
"args": ["/path/to/figma-kiwi-protocol/mcp/server.mjs"],
"env": {
"FIGMA_KIWI_DIR": "/tmp/figma_kiwi"
}
}
}
}
MCP tools: figma_pages, figma_page, figma_node, figma_search, figma_css, figma_texts, figma_components.
Common Patterns
Extract All Text Content
import { buildTree } from 'figma-kiwi-protocol';
import fs from 'fs';
const scenegraph = JSON.parse(
fs.readFileSync('/tmp/figma_kiwi/scenegraph.json')
);
const tree = buildTree(scenegraph.nodeChanges);
function extractText(node, texts = []) {
if (node.type === 'TEXT' && node.characters) {
texts.push({
name: node.name,
text: node.characters,
fontSize: node.fontSize,
guid: node.guid
});
}
if (node.children) {
node.children.forEach(child => extractText(child, texts));
}
return texts;
}
const allTexts = extractText(tree);
console.log(JSON.stringify(allTexts, null, 2));
Batch Rename Layers
import { FigmaSession } from 'figma-kiwi-protocol/session';
import fs from 'fs';
const session = await FigmaSession.connect();
const changes = JSON.parse(fs.readFileSync('./renames.json'));
for (const { guid, newName } of changes) {
const [sessionID, localID] = guid.split(':').map(Number);
await session.rename({ sessionID, localID }, newName);
console.log(`Renamed ${guid} → ${newName}`);
}
await session.close();
Find Components by Name
import { buildTree } from 'figma-kiwi-protocol';
import fs from 'fs';
const scenegraph = JSON.parse(
fs.readFileSync('/tmp/figma_kiwi/scenegraph.json')
);
function findByName(node, search, results = []) {
if (node.name && node.name.toLowerCase().includes(search.toLowerCase())) {
results.push({
guid: node.guid,
name: node.name,
type: node.type
});
}
if (node.children) {
node.children.forEach(child => findByName(child, search, results));
}
return results;
}
const tree = buildTree(scenegraph.nodeChanges);
const buttons = findByName(tree, 'button');
console.log(buttons);
Convert Auto-Layout Frame
import { FigmaSession } from 'figma-kiwi-protocol/session';
const session = await FigmaSession.connect();
const guid = { sessionID: 2010, localID: 16169 };
await session.setStackMode(guid, 'HORIZONTAL');
await session.setPadding(guid, { all: 16 });
await session.setSpacing(guid, 12);
await session.setSizing(guid, 'HORIZONTAL', 'HUG');
await session.setSizing(guid, 'VERTICAL', 'HUG');
await session.close();
Binary Format Reference
commandsBlob Structure
Byte Command Parameters
0x01 MoveTo x(f32) y(f32)
0x02 LineTo x(f32) y(f32)
0x03 ClosePath (none)
0x04 CubicBezier x1(f32) y1(f32) x2(f32) y2(f32) x(f32) y(f32)
0x00 (separator) subpath boundary
vectorNetworkBlob Structure
Header: vertexCount(u32) segmentCount(u32) regionCount(u32)
Per vertex (12 bytes):
flags(u32) x(f32) y(f32)
Per segment (28 bytes):
flags(u32) startVertexIdx(u32) tangentStartX(f32) tangentStartY(f32)
endVertexIdx(u32) tangentEndX(f32) tangentEndY(f32)
Troubleshooting
"CDP WebSocket connection failed"
- Ensure Chrome is running with
--remote-debugging-port=9222
- Get correct CDP URL from
chrome://inspect or http://localhost:9222/json/version
- Check no other process is using port 9222
"No frames captured"
- Verify Figma file is open in Chrome tab
- Reload Figma page after starting capture
- Check CDP_WS_URL points to browser, not a specific tab
"Handshake failed" (write path)
- Run
recon-handshake first to capture cookies
- Cookies expire — rerun recon if writes fail with auth errors
- Ensure Figma file is open in Chrome during recon
"Invalid guid format"
- Guids are
sessionID:localID format (e.g. 2004:16177)
- Find guids in decoded scenegraph JSON
- Session ID changes per Figma session; use current decoded data
SVG paths look incorrect
- Composite icons need transform composition (known limitation)
- For SYMBOL nodes with VECTOR children, extract each VECTOR separately
- Check if node has rotation/translation transforms applied
Mutations not appearing in Figma
- Ensure Figma file is open in browser (WebSocket must be connected)
- Check session.mutate() resolved (server echoed ack)
- Verify guid exists in current scenegraph
- Some properties require specific node types (e.g. stackMode only on FRAME)
MCP tools return empty data
- Run
capture-all-pages and decode first
- Check FIGMA_KIWI_DIR points to directory with scenegraph.json
- Ensure scenegraph.json is valid JSON (run
jq . scenegraph.json)