This is the next installment in my series on building an automation foundation for shipping personal projects at scale, following Getting Claude Code and Codex to collaborate on a single machine. This time I want to write about a setup I built that has AI generate a brand guide before you start any frontend implementation.
Why "front-load" the guide?
There's a pattern almost everyone falls into when building UI for a personal project: "Let me just implement something first and clean up the design later."
And here's what happens. The code Claude generates always carries that default Tailwind vibe — the screen ends up buried in shadows, rounded corners, and uniform card grids, looking like something you've seen a hundred times before. When you tell it "clean and minimal," the design the AI reaches for is always the lowest common denominator.
The problem is an imbalance of information. Without instructions, Claude implements with the library's default settings. When a component has no intent of its own, the framework's intent fills the void.
Turn that around: hand the AI an intentional brand guide before implementation, and its output transforms completely. Decide up front the "this is the only direction" for each project, spell out the intent behind color, typography, motion, gradients, and grain, and then start implementing — the quality of the generated code changes by an order of magnitude.
The tool that automates that "front-loading" is ~/dev/brand-kit/generate.sh.
The big picture of brand-kit
The structure of ~/dev/brand-kit/ is simple.
brand-kit/
├── generate.sh ← main script
├── lib/
│ └── strip_fence.py ← code-fence stripping utility
├── output/
│ └── <slug>/ ← where generated files land
└── README.md
When you pass generate.sh a slug (a project identifier) and a brief (a one-to-three-line summary), up to four files get generated under output/<slug>/.
| File | Contents |
|---|---|
brand-guideline.md |
Worldview, style direction, palette intent, typography strategy, motion principles, Do & Don't |
tokens.css |
oklch-based CSS custom properties |
preview.html |
A single-HTML landing-page fragment that actually applies the tokens |
references.md |
(only with --refs) Real reference URLs gathered via WebSearch |
The basic usage is to hand this whole guide set to the implementer (Claude Code) and say "implement following this guide." With the guide in place first, Claude never has to agonize over choices.
How you actually use it
bash generate.sh <slug> "<summary / who it's for / what kind of page>" [--refs] [--force]
Let me run it on a fictional local-contractor project.
cd ~/dev/brand-kit
bash generate.sh marui-koumuten \
"板橋区の地域工務店のリニューアルHP。50〜60代施主向け。職人の手仕事と地域実績で問い合わせを取る"
When it runs, three files are generated in sequence.
[brand-kit] guideline生成 (試行1/3)…
[brand-kit] ✓ brand-guideline.md
[brand-kit] tokens生成 (試行1/3)…
[brand-kit] ✓ tokens.css
[brand-kit] preview生成 (試行1/3)…
[brand-kit] ✓ preview.html
[brand-kit] 完了: ~/dev/brand-kit/output/marui-koumuten
- brand-guideline.md
- tokens.css
- preview.html
→ preview.html をブラウザで確認。方向が違えば概要を変えて --force で再生成。
If you don't like the direction, rewrite the summary and add --force to rebuild all the files. Add --refs and it also generates a references.md of real reference URLs gathered via WebSearch (which takes a bit more time).
The heart of the design: why split it into 3 calls?
Read generate.sh and you'll see it generates the guideline, tokens, and preview in three separate, independent calls. The internal call() helper looks like this.
# 1コール実行ヘルパ: $1=ラベル $2=プロンプト $3=allowedTools
call() {
local label="$1" prompt="$2" tools="${3:-}" r=""
local toolflag=(); [ -n "$tools" ] && toolflag=(--allowedTools "$tools")
for attempt in 1 2 3; do
echo "[brand-kit] $label (試行$attempt/3)…" >&2
r=$(timeout "$T" "$CLAUDE" -p "$prompt" "${toolflag[@]}" --model "$MODEL" </dev/null 2>/dev/null)
[ -n "$r" ] && ! printf '%s' "$r" | grep -qiE \
'usage limit|rate limit|session limit|hit your session|request timed out' \
&& { printf '%s' "$r"; return 0; }
sleep 3
done
return 1
}
Why not emit everything in a single call? The reason is clear. guideline.md alone is a substantial amount of content, and adding tokens.css and preview.html on top of it pushes the output past the max token count and cuts it off mid-stream. You end up with CSS that ends without a closing brace, or a broken HTML file with no </body>.
There's another benefit to splitting: later calls can take the output of earlier calls as input.
GUIDE=$(head -c 6000 "$GFILE" 2>/dev/null)
# ↑ guideline の先頭6000文字を tokens 生成プロンプトに埋め込む
TOKENS=$(cat "$TFILE" 2>/dev/null)
# ↑ tokens.css 全文を preview 生成プロンプトに埋め込む
The guideline's intent carries into the tokens, and the tokens' values carry into the preview, consistently. The outputs don't scatter — the three files share the same worldview.
Also, each file is skipped if it already exists.
if [ "$FORCE" = 1 ] || [ ! -s "$TFILE" ]; then
# tokens を生成
else
echo "[brand-kit] = tokens.css (既存・skip)"
fi
This is the crux of idempotency. Even if it times out or fails partway through, re-running it fills in only the missing files. If brand-guideline.md succeeds but tokens.css fails, the next run skips brand-guideline.md and resumes from tokens.css.
Pitfalls I hit
Pitfall 1: forget </dev/null and it freezes
The first time I called claude -p from a shell script, I ran into the command never returning.
The cause is stdin. claude -p tries to read piped input. Call it naively inside a script and Claude stalls, waiting on the terminal's standard input. Even wrapped in timeout, it stays blocked for the entire duration until the timeout fires.
# NG: stdin が閉じておらず、対話入力を待ってフリーズする
r=$(timeout 240 "$CLAUDE" -p "$prompt" --model sonnet)
# OK: </dev/null で stdin を閉じる
r=$(timeout 240 "$CLAUDE" -p "$prompt" --model sonnet </dev/null 2>/dev/null)
Note
When you callclaude -pfrom a shell script or launchd,</dev/nullis mandatory. It's not a problem when you type it by hand in an interactive terminal, but for background processes or automation scripts, always include it. The Codex CLI has the same trap in headless execution (see the previous article).
Pitfall 2: session-limit errors produce broken files
When claude -p hits the session limit, it returns an error message instead of CSS or HTML.
You've hit your session limit. Please wait before continuing.
Write that error string straight into tokens.css, and the subsequent preview.html generation receives the error text as its "CSS tokens" and produces garbage HTML. Worse, it gets saved to the file as-is, and on the next run it's skipped as "already exists," leaving it broken forever.
As a countermeasure I put a guard inside the call() helper.
[ -n "$r" ] && ! printf '%s' "$r" | grep -qiE \
'usage limit|rate limit|session limit|hit your session|request timed out' \
&& { printf '%s' "$r"; return 0; }
sleep 3
It returns the output only if it's non-empty and doesn't contain an error pattern. If an error comes back, it waits 3 seconds and retries. Fail three times and it return 1s as a failed call, so it never writes to the file at all.
Pitfall 3: code fences leak into the response
The response from claude -p can include code fences like css ` or `html even when you didn't ask for them. Get a code fence in a CSS file and you can't apply it in a browser as-is.
I pipe each call's output through lib/strip_fence.py to preprocess it.
# lib/strip_fence.py(抜粋)
t = sys.stdin.read().strip()
# ```
{% endraw %}
lang ...
{% raw %}
``` で囲まれている最大ブロックを優先
m = re.search(r'```
[a-zA-Z]*\n(.*?)
```', t, flags=re.DOTALL)
if m:
print(m.group(1).strip() + "\n")
sys.exit(0)
# フェンス無し: 最初の構造的な行以降を採用(前置き散文を落とす)
lines = t.splitlines()
start = 0
for i, ln in enumerate(lines):
s = ln.lstrip()
if s.startswith(("# ", "## ", ":root", "<!--", "<!DOCTYPE", "<html", "<header", "/*", "# 参考")):
start = i
break
print("\n".join(lines[start:]).strip() + "\n")
If there's a fence, it extracts only what's inside; if there isn't, it takes everything from the first "structural line" onward. # (a Markdown heading), :root (CSS), <!-- (HTML), and <html are used to detect the starting line. Prefatory prose like "Below is the CSS:" gets dropped automatically.
R=$(call "tokens生成" "$P" "") && printf '%s\n' "$R" | strip_fence > "$TFILE"
Pitfall 4: cramming everything into one call cuts the output off
In the first version I wrote a single prompt saying "emit the guideline, tokens, and preview all at once."
The guideline came out fine. But tokens.css would end in the middle of :root {, or preview.html would stop without closing a <section> tag. That's the output-token ceiling kicking in.
The problem went away once I split it into three calls, each returning only one file. Splitting made each call shorter, so it finishes comfortably even within the default 240 seconds. The per-call timeout is adjustable via BRAND_KIT_TIMEOUT (default 240 seconds).
The actual output: looking at brand-guideline.md
Here's the style-direction section from a brand-guideline.md that was actually generated.
## 選んだ方向と理由
### 墨付けエディトリアル(Sumitsuke Editorial)
「墨付け(すみつけ)」とは、大工が刃を入れる前に木材に墨壺で正確な基準線を引く所作——
職人の仕事はこの一線から始まる。このサイトのデザイン言語は、その精度と誠実さを視覚に移植する。
和紙のような温かい紙質のベースに、重量のある明朝体見出しを置く。
グリッドを意図的に破り、写真は枠に入れず断ち切る。
グレインは「刷られた紙」の手触りとして機能する。
参照軸は、活版印刷の日本建築専門誌(新建築・住宅建築)の誌面。
なぜこの方向か(3つの根拠):
1. 「ミニマル」は空虚に見える:50-60代には「余白だらけ=やる気がない会社」と読まれるリスクがある
2. 明るい紙白が読みやすさを担保する:温和紙のような地色が50-60代の読み疲れを防ぐ
3. エディトリアルの非対称が「記憶に残る」:均一カードグリッドはどの工務店も使う。
7:5分割・テキストと写真の重なり・引き出し線が「ここは違う」と感じさせる
Rather than "clean and minimal," it's a style direction with a proper name of its own: "Sumitsuke Editorial." It even includes the directions it didn't choose (Japanese modern, dark luxury, Swiss grid) and the reasons why, so the decision about direction remains on record.
When the implementer reads this guide, judgments like "don't use card grids with corner radii of 8px or more," "the line-height for Mincho headings is 1.15," and "CTAs use only the burnt-cedar color, and the verdigris accent is reserved for pull-quote lines" become uniquely determined. With less room for interpretation in the instructions, there's less variance in the output.
The actual output: looking at tokens.css
Here's part of tokens.css.
:root {
/* ─── カラー ───────────────────────────────────────
素材参照: 漆喰・墨・赤土・緑青の錆 / すべて oklch() */
--color-ground: oklch(96.5% 0.008 82); /* 温和紙・漆喰白 — 全体ベース */
--color-ground-alt: oklch(93.0% 0.012 79); /* 素焼きタイル面 — セクション交互 */
--color-ink: oklch(14.0% 0.006 255); /* 墨 — わずかに冷色 */
--color-accent: oklch(40.0% 0.068 165); /* 緑青 — プルクォートライン・リンク */
--color-cta: oklch(48.0% 0.052 60); /* 焼き杉色 — CTAの唯一の強調色 */
The colors are all defined with oklch(). oklch is a perceptually linear color space that expresses a color with three components: lightness L (0–100%), chroma C, and hue H. Unlike hsl, changing only the lightness while keeping the chroma the same doesn't muddy the color, which makes it easy to create derived colors for things like hover states.
Fluid typography is implemented with clamp().
/* ─── 流体テキストスケール ──────────────────────────
50-60代ターゲット → ベースやや大きめ */
--text-base: clamp(1rem, 0.95rem + 0.45vw, 1.125rem); /* 16-18px 本文 */
--text-hero: clamp(3.5rem, 1.50rem + 8.93vw, 7.5rem); /* 56-120px ヒーロー */
In the clamp(minimum, preferred, maximum) form, the size changes smoothly according to the viewport width. The vw coefficient in the preferred value is computed so it reaches the minimum at 320px and the maximum at 1440px.
The grain (paper texture) embeds an SVG feTurbulence filter directly into a CSS variable as a data URI.
/* ─── グレイン ──────────────────────────────────────
刷られた紙の手触り。SVG feTurbulence / 200×200px タイル */
--grain-bg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)' opacity='0.06'/%3E%3C/svg%3E");
You can reproduce "the feel of printed paper" with no external file dependency. The noise generated by feTurbulence is grayscaled with feColorMatrix and layered thinly at opacity='0.06'. The guideline even spells out the places not to apply it (CTA buttons, input forms, on top of photos), and a comment that prevents misuse goes into tokens.css as well.
Gradients, too, are turned into CSS variables, limited to three uses.
/* ① ヒーロー背景: 木と陰(杉色 → 墨 / 対角) */
--gradient-hero: linear-gradient(
135deg,
oklch(28% 0.055 68) 0%,
oklch(12% 0.005 255) 100%
);
/* ② ビネット: 写真下端の墨色フェード(テキスト乗せ用) */
--gradient-vignette: linear-gradient(
to bottom,
transparent 45%,
oklch(14% 0.006 255 / 0.88) 100%
);
/* ③ セクション上端: 赤土の気配 */
--gradient-section-top: linear-gradient(
180deg,
oklch(67% 0.082 68 / 0.10) 0%,
transparent 100%
);
Because the gradient usage is explicitly stated as "limited to three places across the whole site," the implementer won't multiply them unnecessarily.
How to hand it off to implementation
There are three patterns for using the generated guide.
Pattern 1: hand it directly to Claude Code
output/marui-koumuten/ フォルダを「このガイドに従って実装して」と伝える。
Give Claude Code the guide's path and then ask it to implement, and the implementation of color, typography, and grain is generated in line with the guide. A Claude Code that has read the guide can autonomously decide "which color variable to use," "which variable on hover," and "where to apply the grain."
Pattern 2: polish it on claude.ai
Paste preview.html and tokens.css into claude.ai and adjust in natural language. Open preview.html in a browser, point out what bothers you through conversation, and refine the design.
Pattern 3: turn it into a card with /design-sync
The prompt in generate.sh includes an instruction to emit <!-- @dsCard group="Brand" --> as the first line of preview.html. Sync this to a design-system Project on claude.ai with the /design-sync skill, and subsequent Claude Design generations inherit this project's tokens and stay consistent (using /design-sync requires being logged in to claude.ai).
Tuning the model and timeout
There are environment-variable settings at the top of generate.sh.
CLAUDE="${CLAUDE:-~/.local/bin/claude}"
MODEL="${BRAND_KIT_MODEL:-sonnet}"
T="${BRAND_KIT_TIMEOUT:-240}" # 1コールあたりのタイムアウト(秒)
If you prioritize speed, you can switch to haiku. The depth of thinking in the guide drops, but generation gets faster.
# haiku で速度優先
BRAND_KIT_MODEL=haiku bash generate.sh marui-koumuten "..."
# タイムアウトを伸ばす(低速ネット環境など)
BRAND_KIT_TIMEOUT=360 bash generate.sh marui-koumuten "..."
The generation cost is completely free through claude -p (the Claude MAX allowance). No API key or billing setup is required.
Summary
- Front-loading the guide before implementation achieves de-templatization of AI-generated code
-
generate.shis a 3-independent-call design of guideline → tokens → preview. Emit everything in one call and the output gets cut off mid-stream -
claude -prequires</dev/nullwhen run in the background. Forget it and it freezes waiting on stdin - It detects session-limit error strings with
grepand guards against writing broken files -
strip_fence.pyremoves code fences and prefatory prose, saving the CSS and HTML in a form you can use as-is - Each file is skipped if it exists — idempotency means a re-run fills in only the missing pieces
-
tokens.cssdefinesoklch(),clamp(), and thefeTurbulencegrain all as CSS variables. Gradients are limited to three places across the whole site - The generation cost is completely free on the Max allowance
Written by **Lily* — I ship iOS apps and automate my content stack with Claude Code.
Follow along: Portfolio · X · GitHub*
Top comments (1)
I really like the idea of treating a brand guide as an input artifact instead of an afterthought. In my experience, AI-generated UIs become much more consistent when design intent exists before the first component is created.
One thing I’d consider adding is a validation loop after generation. Instead of stopping at brand-guideline → tokens → preview, I’d have another agent (or review step) check accessibility (WCAG contrast), typography scale, responsive behavior, token consistency, unused variables, and whether the generated preview actually follows the written design principles. That turns the process from generation into generation + verification.
I also like the three-call approach. Besides avoiding token limits, it creates a clean dependency chain where each artifact becomes versionable and independently regenerable. That’s a pattern that scales well beyond branding.