← 記事一覧

コーディングエージェントにデザイントークンをハードコードさせない方法

エージェントはデザインシステムを無視しているのではありません。多くのリポジトリには、無視できるものが最初から存在しません。

約9分

設定画面を頼んだら bg-[#2563eb]p-[13px] が返ってきました。直しました。翌週、別のファイルで同じことが起きました。

「モデルが雑だから」と説明されがちですが、実際にそうであることは稀です。多くのリポジトリには、デザインシステムが機械可読な形で書かれていません。だからエージェントは唯一できることをします — 周囲のコードを見て、もっともらしい値を推測して書く。与えられた材料に対しては正しく振る舞っています。

この見方が重要なのは、直す場所が分かるからです。より良いプロンプトではなく、リポジトリの中にあります。

まず、いま何があるかを数える

ルールを書く前に、コードが実際に何をしているかを見ます。任意値を数えてください:

# Tailwind の任意値
grep -rhoE '\[#[0-9a-fA-F]{3,8}\]' src | sort | uniq -c | sort -rn

# 一回きりの余白
grep -rhoE '\b[pm][xytblr]?-\[[0-9]+px\]' src | sort | uniq -c | sort -rn

検証用に持っているAI製の小さなアプリ(7画面・12コンポーネント・デザインシステム無し)でこれを回すと、色が22種類・余白が15種類出てきます。誰も22色を選んでいません。もっともらしい推測が1つずつ積もった結果です。

数字が論拠になります。動機を説明できないルールは、最初に面倒になった時点で捨てられます。

手順1 — スケールをコードで宣言する

いま数えた値から、ほぼ同じものをまとめて、1つのファイルで名前を与えます。CSS カスタムプロパティならどこでも動き、ビルド手順も要りません:

@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;
}

見た目でなく役割で名付けます--color-brand はブランド変更を生き延びますが、--color-blue は値が変わった瞬間に嘘になります。似た2つのうちどちらを使うべきかも、エージェントには判断できません。

手順2 — エージェントが読む場所にルールを書く

エージェントはリポジトリの規約ファイルを読みます — CLAUDE.mdAGENTS.md.cursorrules。頭の中や Notion にあるルールは、ループの中にありません。

ルールと理由、そしてほぼ全員が飛ばす部分 — 反例 を書きます:

## 色と余白

色と余白は必ずトークン経由で参照する。

理由: 直値はドリフトの最も一般的な形。トークンを通すことで、
ブランド変更が一度に全体へ届く。

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

Don't の行は、ルール本文より働きます。具体的な形を持つ禁止は、抽象的な原則よりはるかに適用しやすいからです。

手順3 — 飛ばせない検査を置く

書かれたルールは風化します。急いでいて、エージェントは自信満々で、値は結局入ります。残るのは自動で走る検査だけです。

最小構成は CI か pre-commit に数行です。差分に任意の色が現れたら落とします:

# ステージされた差分に生の hex があれば落とす
if git diff --cached | grep -qE '\+.*\[#[0-9a-fA-F]{3,8}\]'; then
  echo "差分に生の色があります — styles.css のトークンを使ってください"
  exit 1
fi

これが生き残るかは2点で決まります。全体でなく差分を見ること — 初日に既存の違反を何百件も報告するゲートは、翌日には消されます。そして 人だけでなくエージェントが向き合う場所に置くこと。pre-commit は人を止めますが、エージェントには何も教えません。

本当に効く瞬間

最も効くのは、エージェントが「終わった」と言う瞬間です。Claude Code には Stop フックがあり、まさにそこで発火します。終了コード 2 で終わると停止がブロックされ、stderr がそのまま「次に直すもの」としてエージェントに渡ります。それ以外(exit 1 を含む)はフックのエラー扱いになり、モデルには届きません。

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

この終了コードが要点です。「いつか人が読む検査」を「エージェントが返す前に自分で直す訂正」に変えます。

手順4 — いつ使わないかを書く

トークンができると、失敗の形が変わります。エージェントは hex を発明しなくなり、代わりに間違ったトークンを選ぶようになります — 状態を表す色を強調に使う、面の色に意味を持たせる。

直し方は、名前だけでなく境界を書くことです:

--color-signal — 色が「これは生きている」を意味する唯一の場所。
  リンク・アクティブ状態・記録中の表示。

  使わない: ボタンの背景(強調は黒)。
  使わない: 装飾や大きな塗り — signal が意味を失う。

「いつ使わないか」はデザインシステムで最も希少で、エージェントには最も役に立ちます。名前は何があるかを伝え、境界は選び方を伝えます。多くのコンポーネントライブラリは前者だけを書いて後者を飛ばします。

効いたときの見え方

先ほどのフィクスチャに手順1〜4を当てると、数字はこう動きます:

  • 宣言トークン: 0 → 9
  • コード中の任意値: 12 → 0
  • ゲートのエラー: 13 → 0

重要なのは個々の数字ではなく、そもそも数えられるようになったことです。スケールを宣言した時点で、「うちのデザインシステムは保てているか」が意見の問題でなくなります。

面倒になるところ

上の手順はすべて手作業でできますし、一度は手でやる価値があります。続かないのは維持のほうです — スプリントごとに数え直す、規約ファイルをコードに追随させる、4つのコンポーネントが色しか違わなくなったことに気づく、そして6週間前になぜその名前にしたかを思い出す。

その部分のために Harnd を作っています。既にあるコードからトークンとコンポーネントを読み取り、エージェントが読む文書を書き出し、エージェントが「終わった」と言う瞬間に検査を走らせます。早期アクセスを受付中です

1つだけ持ち帰るなら: エージェントはデザインシステムを無視しているのではなく、書かれていないものを読めないだけです。エージェントが見る場所に書き、飛ばせない場所で検査してください。

エージェントがUIを書いても、デザインの一貫性を保つ。