← All articles

How to stop your coding agent from hardcoding your design tokens

Your agent is not ignoring your design system. In most repositories, there is nothing for it to ignore.

9 min read

You asked for a settings page. You got bg-[#2563eb] and p-[13px]. You fixed it. Next week, in a different file, it came back.

The usual explanation is that the model is careless. That is rarely what is happening. In most repositories there is no machine-readable statement of what the design system is, so the agent does the only thing it can: it looks at the surrounding code, infers a plausible value, and writes it. It is behaving correctly given what it was given.

That reframing matters, because it tells you where the fix lives. Not in a better prompt — in the repository.

First, measure what you already have

Before writing rules, find out what the code is already doing. Grep your codebase for arbitrary values and count the distinct ones:

# Tailwind-style arbitrary values
grep -rhoE '\[#[0-9a-fA-F]{3,8}\]' src | sort | uniq -c | sort -rn

# one-off spacing
grep -rhoE '\b[pm][xytblr]?-\[[0-9]+px\]' src | sort | uniq -c | sort -rn

On a small AI-written app we keep as a test fixture — seven screens, twelve components, no design system — this returns 22 distinct colours and 15 distinct spacing values. Nobody chose 22 colours. They accumulated, one plausible guess at a time.

The number is the argument. A rule you cannot motivate gets dropped the first time it is inconvenient.

Step 1 — Declare the scale, in code

Take the values you just counted, collapse the near-duplicates, and give them names in one file. CSS custom properties work everywhere and need no build step:

@theme {
  --color-brand: #2563eb;
  --color-brand-hover: #1d4ed8;
  --color-surface: #ffffff;
  --color-border: #e5e7eb;
  --color-text-primary: #111827;

  --spacing-2: 8px;
  --spacing-3: 12px;
  --spacing-4: 16px;
}

Name by role, never by appearance. --color-brand survives a rebrand; --color-blue becomes a lie the first time the value changes — and it gives the agent no way to choose between two similar tokens.

Step 2 — Write the rule where the agent reads

Agents read the conventions files in your repository — CLAUDE.md, AGENTS.md, .cursorrules. A rule in your head, or in a Notion page, is not in the loop.

Write the rule, the reason, and — this is the part almost everyone skips — a counter-example:

## Colour and spacing

Reference every colour and spacing value through a token.

Why: hardcoded values are the most common drift pattern. Routing them
through tokens is what makes a rebrand land everywhere at once.

- Do:    className="bg-[var(--color-brand)] p-[var(--spacing-4)]"
- Don't: className="bg-[#2563eb] p-[13px]"

The Don't line does more work than the rule. A prohibition with a concrete shape is far easier to apply than an abstract principle.

Step 3 — Add a check the agent cannot skip

A written rule decays. Someone is in a hurry, the agent is confident, and the value lands anyway. What holds is a check that runs on its own.

A minimal version is a few lines in CI or a pre-commit hook — fail when an arbitrary colour appears in a diff:

# fails if a raw hex lands in the staged diff
if git diff --cached | grep -qE '\+.*\[#[0-9a-fA-F]{3,8}\]'; then
  echo "Raw colour in diff — use a token from styles.css"
  exit 1
fi

Two things decide whether this survives. Check the diff, not the whole tree — a gate that reports hundreds of pre-existing violations on day one gets deleted on day two. And put it where the agent, not only the human, has to face it. A pre-commit hook stops a person; it does not teach the agent anything.

The moment that actually works

The highest-leverage point is the instant the agent says it is done. Claude Code exposes a Stop hook that fires exactly there. If the hook exits with status 2, the agent is blocked from stopping and your stderr is handed back to it as the next thing to fix. Anything else, including exit 1, is treated as a hook error and never reaches the model.

{
  "hooks": {
    "Stop": [
      { "hooks": [{ "type": "command", "command": "./scripts/check-tokens.sh" }] }
    ]
  }
}

That exit code is the whole trick. It converts a check that a human eventually reads into a correction the agent performs before it hands the work back.

Step 4 — Say when not to use each token

Once tokens exist, the failure mode changes. The agent stops inventing hex values and starts picking the wrong token — a status colour used for emphasis, a surface colour used to mean something.

The fix is to write the boundary, not just the name:

--color-signal — the one place colour means "this is alive":
  links, active state, recording indicators.

  Not for: button backgrounds (emphasis is black).
  Not for: decoration or large fills — that makes the signal meaningless.

"When not to use it" is the rarest thing in a design system and the most useful to an agent. A name tells it what exists; a boundary tells it how to choose. Most component libraries document the first and skip the second.

What this looks like when it works

On the fixture above, applying steps 1–4 moves the numbers like this:

  • Declared tokens: 0 → 9
  • Arbitrary values in code: 12 → 0
  • Gate errors: 13 → 0

The point is not the specific numbers — it is that they are countable at all. Once the scale is declared, "is our design system holding?" stops being a matter of opinion.

Where this gets tedious

Every step above is doable by hand, and worth doing by hand once. What does not scale is the upkeep: re-counting after each sprint, keeping the conventions file in step with the code, noticing that four components now differ only in colour, and remembering why a token was named the way it was six weeks ago.

That is the part we build Harnd for: it reads the tokens and components out of the code you already have, writes the brief your agent reads, and runs the check at the moment the agent says it is done. Early access is open.

If you only take one thing: the agent is not ignoring your design system — it cannot read one that was never written down. Write it where the agent looks, and check it where it cannot skip.

Keep design consistent while your agents write the UI.