The first result: how we defined the token governance for the Harness Design System

In those three days, the start of the two weeks of exclusive dedication to the project that we began on a Wednesday, we defined a governance process and a way to create, edit, and update the tokens, both in Figma and in code.
We made a point of solving governance right away, because the goal was never just to make it work once. It was to deliver a scalable solution that other people on the team can keep maintaining afterwards.
Day one: getting everyone on the same page
The kickoff was about alignment. We talked about the current state of the design system and the next steps, and we put designers and developers on the same page before any line of code. We also started the central experiment of the week: applying harness documentation to the tokens using Claude Code.
In parallel, the development team began studying a way to have a GitHub repository as the source of truth for the tokens: a single place, updated whenever needed, that React and Flutter will consume from.

We also ran an important POC: the mapping of the token change. The review touched many things, so we created a map that shows each current token in the system and its new equivalent. The idea is simple: with this map, AI can update the tokens in the code much faster than we could by hand.

Bind: the problem with no clean solution
Before the problem, I need to explain a term I will use a lot: bind. Binding means "tying" a property of an element to a token, a variable from the design system, instead of leaving a loose value typed by hand. In our case, binding (or fixing a binding) means making sure every color, border, and corner radius of a button is tied to the right token: not to a value typed by hand, not to the wrong token, and not to a variable from another library. This is what keeps the component consistent and easy to maintain, and further down the road, it is what reduces time and cost.

It was exactly at this stage that a question came up that took a good amount of time: we needed to bind the components to the new tokens in Figma. It sounds simple. It was not.
First I tried Claude Code via MCP. It did not work. The next solution was to ask Claude to generate scripts and use the Scripter plugin to run the bind. That did not work well either. On the second day I received an update from Figma Agents, and after iterating, I arrived at a bind prompt that worked better. Not perfect, but it really helped.
The prompt looked like this:
Bind the semantic variables to the visual properties of the selected component, following these rules:
Corner radius: bind each corner individually (topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius) to the matching variable in the Radius collection (e.g. value 4 → sm, 8 → md, 12 → lg, 16 → xl). Apply to all nodes with cornerRadius > 0 and no binding: COMPONENTs, FRAMEs, INSTANCEs, RECTANGLEs.
Stroke weight: bind each side individually (strokeTopWeight, strokeBottomWeight, strokeLeftWeight, strokeRightWeight) to the matching variable in the Stroke Width collection (e.g. value 0.5 → sm, 1 → md, 2 → lg, 4 → xl). Apply to any node with strokes and no strokeWeight binding. For icon VECTORs, use setBoundVariable('strokeWeight', ...) directly.
Icon stroke colors: icons that render through a stroke (VECTORs with no fill, only strokes) must have the stroke color bound to a variable from the Colour Usage > Icon/* collection. Identify the correct variable from the variant context (button color and state). Do not touch the fills of INSTANCEs that wrap icons: if the icon is stroke-based, clear any residual fill on the wrapper instance instead of binding it.
Important: before running, scan all variants of the component set to map what has no binding. Split the operations into separate scripts by property type to avoid timeouts (corner radius first, then stroke weight on components, then on internal VECTORs). Check the result by exporting an image and inspecting node.boundVariables (note: cornerRadius is stored as topLeftRadius, etc., and strokeWeight as strokeTopWeight, etc. Check these fields, not cornerRadius directly).

But even this prompt was not the definitive solution. That came later, and in a way that reinforces one of the things I defend most. We built our own plugin for Figma, made in a multidisciplinary way, with designers and developers together. That collaboration is what made a real solution possible: the plugin delivered much better results, tailored exactly to what we needed, instead of depending on a generic script or a prompt that sometimes works and sometimes does not. And it did more than the bind: it also started exporting the tokens the way we wanted.

This is the kind of thing that does not show up on the nice slide. You try one path, it fails, you try another, it fails again, and what finally unlocked it was the team sitting together and building the tool that was missing. That is part of the job.
In the same period, we also found structural problems in the semantics of the token names and adjusted them. A token name matters more than it seems when the reader is an LLM.
Day three: the schema, the moment that unlocked everything
Day three was the most important one. It was when we stopped to properly structure how the harness would be built: where this information would live, how it would be updated, and who would touch it. That is when we defined our schema standard for the token descriptions, and finally applied the Harness Design layer.

This is the schema example we created:
{
"purpose": "High-emphasis solid brand fill: brand identity and primary action. Base Bold step: the leading surface (primary action, selected item, solid badge).",
"useWhen": ["Primary action or the element of greatest brand prominence."],
"doNotUseWhen": [
{ "scenario": "Signaling status (success, error, attention, process)", "reason": "the brand does not communicate state; using it for status creates an ambiguous reading of severity", "alternative": "Background|Text|Icon/Status/*" },
{ "scenario": "Informational content that is not the main action", "alternative": "Status/Info/* (same color, correct semantic role)" }
],
"pairsWith": { "text": "Text/Brand/Inverse", "icon": "Icon/Brand/Inverse" },
"note": null
}
Creating this schema was only possible because of a strategic decision: at this first stage, we put the documentation directly in the description of each variable, inside Figma itself. And we validated the piece that was missing: Claude Code can pull this information along with the JSON of the variables. In other words, both the tokens and the harness schema will live in Figma.
I want to say one thing clearly: this structure works fully for our context, with the tools and the team we have. It is not the definitive architecture for everyone. It is the one that makes sense for us right now.
The flow we decided on

With the schema defined, the flow for applying the harness to the tokens looks like this:
Define the schema first. Before anything else, the documentation contract that goes in the description field, using the W3C DTCG as a base (which already guarantees name, title, and value as static fields) and adding the harness properties on top. This schema is made available to Claude in Prolog Claude, our repository with the Prolog information that Claude consumes, in the file CLAUDE.md.

Initial load via Claude Code: the experiment we ran. With the schema ready, we asked Claude Code to enter Figma via MCP and analyze how each token is being used. From that context, it pulls what it needs to write the schema for each of the roughly 600 tokens. After writing the schemas, it goes back to Figma, also via MCP, and inserts each one into the description of the corresponding variable, instead of someone filling in 600 tokens by hand. We do not expect it to come out perfect on the first try; the idea is to test how effective this path is and refine it later.

Maintenance when a token changes. The designer creates or edits the variable in Figma and writes the description following the schema. A skill in Claude Code makes sure they do not forget to apply the schema, do not erase anything that already exists, and edit it the right way.

Export and commit, the part that is truly manual. A parser in Claude Code reads the descriptions via the Figma MCP, breaks down the harness, downloads the current JSON from the repository, compares the diff, and commits to Prolog Claude. This JSON is what React and Flutter consume afterwards.

Why manual, and not the "ideal" version
The obvious question is: why not automate everything? We did design more automated versions, including one that centralized everything in a tool of our own. But we reached a conclusion worth recording: updating a token is rare. It happens less than once a month, and it is punctual.
Building the full automation now was not worth it. Every automatic step opens the door to an edge case: the process breaks in the middle, the AI edits the wrong thing, someone forgets to export, and the cost of designing this properly, handling each of these failures, is high. For a task that happens once a month, the manual path is cheaper and more reliable.
This connects directly to what I wrote in "Before AI-first, human-first". The ideal solution on paper is beautiful. The real solution is the one the team can operate today, without drowning in exception cases. We chose the real one and spent a good amount of discussion time in a single brainstorm to arrive at this apparently simple choice. It was worth every minute.
Harness for tokens is not harness for components
One thing became clear in this process: harness for tokens is different from harness for components. Normally, the LLM builds interfaces using the components that already exist; it does not keep creating new components all the time. The need for it to understand how to use a token shows up more in the scenario where it has to make decisions while creating something from scratch, with no ready component to reuse.
This scenario tends to become rarer as we apply harness to the components and expand the number of usable components. That is also why the token schema ended up smaller than I imagined: it does not need to cover everything, because most interface decisions will come from the components, not from loose tokens.
What comes next: refinement and tests for harness on tokens
What we will do next is test how the LLM is understanding the design tokens and validate the whole flow we created for updating tokens. After that, we finally get to the tests with components.



Comments