design-token-guard
The problem this solves
A hardcoded color renders identically to the token it should have used.
style={{ background: "#07090c" }} and style={{ background: "var(--tl-tooltip)" }}
produce the same pixels. So every render-level gate — visual review, e2e,
screenshots, "console is clean" — passes a component that has silently bypassed
the design system. The violation only exists in source.
That is why inline styles and hardcoded CSS accumulate invisibly until someone
eyeballs the code weeks later and burns hours on a token refactor. The fix is a
source-level gate: a deterministic check that reads the diff for styling
that bypasses the token system, run before the code is declared done, and
wired into the repo so it can't regress.
This skill is that gate. It does two things:
- Audit — run the bundled checker (
scripts/check_design_tokens.py) to find
every inline style and hardcoded color, each mapped to the exact token to use.
- Enforce — scaffold the check into the repo (config + ESLint rule +
pre-commit hook +
lint:tokens script) so the gate runs on every commit/CI,
not just when an agent remembers to look.
It is dynamic: it auto-discovers whatever token system the project already
uses and derives the rules from it. TruthLens and a vanilla Vue app are two
configs of one skill, not two skills.
Step 1 — Audit
Run the checker from the project root. It needs no flags for a first pass:
python3 <skill-dir>/scripts/check_design_tokens.py --root .
What it does automatically:
Discovers the token source — scans for CSS custom properties
(--name: …), SCSS/Less vars ($name, @name), JS/TS theme objects, or
design-token JSON. From those it builds a color-value → token map.
Scans the source tree (.tsx/.ts/.jsx/.js/.vue/.svelte/.astro/.html),
skipping node_modules, build dirs, config files, and the token files
themselves.
Reports each finding as file:line:col, the offending snippet, and — when
the literal matches a declared token — the exact fix:
no-hardcoded-color (error) — 2
src/components/schedule/EventBar.tsx:69:17
fill="#0E1116"
→ hardcoded color #0E1116 is var(--tl-bg-0) · use var(--tl-bg-0)
Useful flags:
--staged — only git-staged files (this is what the pre-commit hook uses).
--json — machine-readable output for a gate/CI to parse ({ ok, summary, findings }).
PATHS… — limit to specific files/dirs (e.g. just the component you changed).
--config <path> — explicit config location.
Exit code is 1 when there are error-severity findings, 0 when clean — so
it drops straight into a gate or CI step.
Step 2 — Interpret and fix
Two outcomes per color finding, and they need different fixes — don't blur them:
- "… is
var(--token)" — the literal duplicates an existing token. Replace
it with the token reference. Mechanical and safe.
- "… is not a declared token" — the color was never tokenized. This is the
more important signal: either it's a genuinely new design value (add it to the
token source with a real name, then reference it) or it's an off-palette
mistake (use the nearest existing token). Don't paper over it by leaving the
literal — that's how palettes rot.
For no-inline-style findings, move the value into a token or a utility class.
Inline styles whose values are all dynamic (var(--…), JS expressions) are
allowed by default — the rule fires on hardcoded literals, not on the
mechanism. Use "inlineStyleMode": "strict" only if the project bans inline
style attributes outright.
Step 3 — Enforce (scaffold into the repo)
A one-time audit doesn't stop regression. Wire the gate into the repo so it runs
without anyone remembering. Read references/scaffolding.md for the full
procedure; the short version:
- Write
.design-guard.json at the repo root (template:
assets/design-guard.config.json). Set tokenSources explicitly so
discovery is deterministic in CI, and turn on any project-specific rules.
- Add the checker to the repo — copy
scripts/check_design_tokens.py into
the project's scripts/, or reference the skill path. Add a script:
"lint:tokens": "python3 scripts/check_design_tokens.py --root .".
- Pre-commit hook — install
assets/pre-commit (runs --staged, blocks the
commit on error-severity findings). Wire via Husky/lefthook if present, else a
plain .git/hooks/pre-commit.
- ESLint editor-time subset — merge
assets/eslint-tokens.snippet.mjs into
the repo's flat config (create eslint.config.mjs if absent). ESLint gives
in-editor squiggles for the patterns it expresses well (inline-style literals,
forbidden radius/partisan-color classes). The Python checker stays the
authority — it's the only layer that can do the value → token mapping, so
pre-commit/CI run that, and ESLint is fast feedback, not the source of truth.
- CI — add a
lint:tokens step to the pipeline (assets/ci-step.yml shows a
GitHub Actions example).
Tell the user which layers you installed and which you skipped (e.g. "no Husky
here, used a raw git hook").
The rule set
Two universal rules, on by default (these are the "no inline CSS" core):
| rule |
catches |
default |
no-hardcoded-color |
#hex, rgb()/rgba(), hsl()/hsla() literals anywhere a token belongs — inline styles, SVG fill/stroke, JS color strings, Tailwind arbitrary [#…] |
error |
no-inline-style |
style={{…}} / style="…" / :style / [style] carrying hardcoded literals (React/Vue/Svelte/Angular/HTML) |
warn |
A project-specific library, off by default — opt in via config when a repo
has these conventions:
| rule |
catches |
no-class-in-svg |
utility/Tailwind classes on <svg> primitives (rect, path, g, …) where styling should be token attributes |
restricted-radius |
border-radius above maxRadiusPx, or rounded-{md,lg,xl,…} classes |
forbidden-colors |
configured regexes — banned second accent, partisan red/blue, etc. |
no-arbitrary-tailwind |
Tailwind arbitrary values — text-[10px], max-w-[1100px], leading-[1.75] — that bypass the spacing/type scale. Arbitrary colors (border-[rgba(…)]) are already caught by no-hardcoded-color, so this rule skips them. *-[var(--token)] is allowed. |
Each rule's severity is "error" | "warn" | "off". Errors fail the gate
(exit 1); warnings report but pass. Full config reference and per-rule notes:
references/config.md.
Using it as an agent / orchestrator gate
When a frontend build runs under an orchestrator or agent team, this is a hard
source-level wave-gate, complementary to the render-level gates (render-sanity,
ux-review) — those check pixels, this checks source, and a build needs both:
- A frontend-agent runs
--json against the files it changed before
reporting done; error-severity findings mean the task isn't done.
- The orchestrator runs it at the wave gate alongside typecheck/test. Parse
summary.errors; non-zero blocks the wave and routes back to the owning agent.
See references/wiring-into-orchestrator.md for the exact gate snippet and how
this plugs into the orchestrator's Definition of Done.
Reference files
references/config.md — full .design-guard.json schema, every field, per-rule notes, and adapting to non-CSS-variable token systems (SCSS, JS theme, Style Dictionary).
references/scaffolding.md — step-by-step repo enforcement: config, hook, ESLint, CI, with the Husky/lefthook/raw-hook decision.
references/wiring-into-orchestrator.md — the agent self-check + orchestrator wave-gate snippets, and the Definition-of-Done line this closes.
1---2name: design-token-guard3description: Source-level gate that prevents inline styles and hardcoded CSS from bypassing a project's design-token system. Use whenever frontend work touches colors, styling, or themes — before committing or declaring a UI task done, when auditing for hardcoded hex/rgb/inline styles, when setting up enforcement so inline CSS can't slip in again, or as an orchestrator/agent wave-gate. Trigger on: "inline CSS", "inline styles", "hardcoded colors", "hardcoded hex", "style={{}}", "theme/token drift", "design tokens", "we keep shipping inline styles", "lint for tokens", "why did this get through review". Framework- and stack-agnostic (React/JSX, Vue, Svelte, Angular, Astro, HTML; CSS variables, SCSS/Less, JS theme objects, design-token JSON) — auto-discovers the project's tokens; NOT specific to any one repo or to Tailwind. Don't skip it because a render review passed: render gates can't see a hardcoded color — it renders identically to the token.4---56# design-token-guard78## The problem this solves910A hardcoded color renders *identically* to the token it should have used.11`style={{ background: "#07090c" }}` and `style={{ background: "var(--tl-tooltip)" }}`12produce the same pixels. So every **render-level** gate — visual review, e2e,13screenshots, "console is clean" — passes a component that has silently bypassed14the design system. The violation only exists in **source**.1516That is why inline styles and hardcoded CSS accumulate invisibly until someone17eyeballs the code weeks later and burns hours on a token refactor. The fix is a18**source-level gate**: a deterministic check that reads the diff for styling19that bypasses the token system, run *before* the code is declared done, and20wired into the repo so it can't regress.2122This skill is that gate. It does two things:23241. **Audit** — run the bundled checker (`scripts/check_design_tokens.py`) to find25 every inline style and hardcoded color, each mapped to the exact token to use.262. **Enforce** — scaffold the check into the repo (config + ESLint rule +27 pre-commit hook + `lint:tokens` script) so the gate runs on every commit/CI,28 not just when an agent remembers to look.2930It is **dynamic**: it auto-discovers whatever token system the project already31uses and derives the rules from it. TruthLens and a vanilla Vue app are two32*configs* of one skill, not two skills.3334## Step 1 — Audit3536Run the checker from the project root. It needs no flags for a first pass:3738```bash39python3 <skill-dir>/scripts/check_design_tokens.py --root .40```4142What it does automatically:43- **Discovers the token source** — scans for CSS custom properties44 (`--name: …`), SCSS/Less vars (`$name`, `@name`), JS/TS theme objects, or45 design-token JSON. From those it builds a `color-value → token` map.46- **Scans the source tree** (`.tsx/.ts/.jsx/.js/.vue/.svelte/.astro/.html`),47 skipping `node_modules`, build dirs, config files, and the token files48 themselves.49- **Reports** each finding as `file:line:col`, the offending snippet, and — when50 the literal matches a declared token — the exact fix:5152 ```53 no-hardcoded-color (error) — 254 src/components/schedule/EventBar.tsx:69:1755 fill="#0E1116"56 → hardcoded color #0E1116 is var(--tl-bg-0) · use var(--tl-bg-0)57 ```5859Useful flags:60- `--staged` — only git-staged files (this is what the pre-commit hook uses).61- `--json` — machine-readable output for a gate/CI to parse (`{ ok, summary, findings }`).62- `PATHS…` — limit to specific files/dirs (e.g. just the component you changed).63- `--config <path>` — explicit config location.6465Exit code is `1` when there are **error**-severity findings, `0` when clean — so66it drops straight into a gate or CI step.6768## Step 2 — Interpret and fix6970Two outcomes per color finding, and they need different fixes — don't blur them:7172- **"… is `var(--token)`"** — the literal duplicates an existing token. Replace73 it with the token reference. Mechanical and safe.74- **"… is not a declared token"** — the color was never tokenized. This is the75 more important signal: either it's a genuinely new design value (add it to the76 token source with a real name, then reference it) or it's an off-palette77 mistake (use the nearest existing token). Don't paper over it by leaving the78 literal — that's how palettes rot.7980For `no-inline-style` findings, move the value into a token or a utility class.81Inline styles whose values are all dynamic (`var(--…)`, JS expressions) are82**allowed** by default — the rule fires on hardcoded literals, not on the83mechanism. Use `"inlineStyleMode": "strict"` only if the project bans inline84style attributes outright.8586## Step 3 — Enforce (scaffold into the repo)8788A one-time audit doesn't stop regression. Wire the gate into the repo so it runs89without anyone remembering. Read `references/scaffolding.md` for the full90procedure; the short version:91921. **Write `.design-guard.json`** at the repo root (template:93 `assets/design-guard.config.json`). Set `tokenSources` explicitly so94 discovery is deterministic in CI, and turn on any project-specific rules.952. **Add the checker to the repo** — copy `scripts/check_design_tokens.py` into96 the project's `scripts/`, or reference the skill path. Add a script:97 `"lint:tokens": "python3 scripts/check_design_tokens.py --root ."`.983. **Pre-commit hook** — install `assets/pre-commit` (runs `--staged`, blocks the99 commit on error-severity findings). Wire via Husky/lefthook if present, else a100 plain `.git/hooks/pre-commit`.1014. **ESLint editor-time subset** — merge `assets/eslint-tokens.snippet.mjs` into102 the repo's flat config (create `eslint.config.mjs` if absent). ESLint gives103 in-editor squiggles for the patterns it expresses well (inline-style literals,104 forbidden radius/partisan-color classes). The Python checker stays the105 authority — it's the only layer that can do the `value → token` mapping, so106 pre-commit/CI run *that*, and ESLint is fast feedback, not the source of truth.1075. **CI** — add a `lint:tokens` step to the pipeline (`assets/ci-step.yml` shows a108 GitHub Actions example).109110Tell the user which layers you installed and which you skipped (e.g. "no Husky111here, used a raw git hook").112113## The rule set114115Two **universal** rules, on by default (these are the "no inline CSS" core):116117| rule | catches | default |118|---|---|---|119| `no-hardcoded-color` | `#hex`, `rgb()/rgba()`, `hsl()/hsla()` literals anywhere a token belongs — inline styles, SVG `fill`/`stroke`, JS color strings, Tailwind arbitrary `[#…]` | error |120| `no-inline-style` | `style={{…}}` / `style="…"` / `:style` / `[style]` carrying hardcoded literals (React/Vue/Svelte/Angular/HTML) | warn |121122A **project-specific library**, off by default — opt in via config when a repo123has these conventions:124125| rule | catches |126|---|---|127| `no-class-in-svg` | utility/Tailwind classes on `<svg>` primitives (`rect`, `path`, `g`, …) where styling should be token attributes |128| `restricted-radius` | border-radius above `maxRadiusPx`, or `rounded-{md,lg,xl,…}` classes |129| `forbidden-colors` | configured regexes — banned second accent, partisan red/blue, etc. |130| `no-arbitrary-tailwind` | Tailwind arbitrary *values* — `text-[10px]`, `max-w-[1100px]`, `leading-[1.75]` — that bypass the spacing/type scale. Arbitrary *colors* (`border-[rgba(…)]`) are already caught by `no-hardcoded-color`, so this rule skips them. `*-[var(--token)]` is allowed. |131132Each rule's severity is `"error" | "warn" | "off"`. Errors fail the gate133(exit 1); warnings report but pass. Full config reference and per-rule notes:134`references/config.md`.135136## Using it as an agent / orchestrator gate137138When a frontend build runs under an orchestrator or agent team, this is a **hard139source-level wave-gate**, complementary to the render-level gates (render-sanity,140ux-review) — those check pixels, this checks source, and a build needs both:141142- A **frontend-agent** runs `--json` against the files it changed *before*143 reporting done; error-severity findings mean the task isn't done.144- The **orchestrator** runs it at the wave gate alongside typecheck/test. Parse145 `summary.errors`; non-zero blocks the wave and routes back to the owning agent.146147See `references/wiring-into-orchestrator.md` for the exact gate snippet and how148this plugs into the orchestrator's Definition of Done.149150## Reference files151152- `references/config.md` — full `.design-guard.json` schema, every field, per-rule notes, and adapting to non-CSS-variable token systems (SCSS, JS theme, Style Dictionary).153- `references/scaffolding.md` — step-by-step repo enforcement: config, hook, ESLint, CI, with the Husky/lefthook/raw-hook decision.154- `references/wiring-into-orchestrator.md` — the agent self-check + orchestrator wave-gate snippets, and the Definition-of-Done line this closes.