<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SeungHo Lee</title>
    <description>The latest articles on DEV Community by SeungHo Lee (@dev_sh).</description>
    <link>https://dev.to/dev_sh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4012843%2F640ada14-8eab-4af1-9d27-b4c08d90e3af.png</url>
      <title>DEV Community: SeungHo Lee</title>
      <link>https://dev.to/dev_sh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dev_sh"/>
    <language>en</language>
    <item>
      <title>I stopped babysitting my AI frontend agent — so I built it guardrails</title>
      <dc:creator>SeungHo Lee</dc:creator>
      <pubDate>Mon, 06 Jul 2026 08:16:51 +0000</pubDate>
      <link>https://dev.to/dev_sh/i-stopped-babysitting-my-ai-frontend-agent-so-i-built-it-guardrails-4bgf</link>
      <guid>https://dev.to/dev_sh/i-stopped-babysitting-my-ai-frontend-agent-so-i-built-it-guardrails-4bgf</guid>
      <description>&lt;p&gt;Claude writes frontend code faster than I can read it. That turned out to be the problem.&lt;/p&gt;

&lt;p&gt;Here is a normal afternoon from a few weeks ago. I ask for a feature, watch a dozen files show up, and then spend the next twenty minutes playing security guard. Did it just run &lt;code&gt;git add .&lt;/code&gt; and sweep my &lt;code&gt;.env&lt;/code&gt; into the commit? Is that a real component or another screen of purple gradients and drop shadows? Did it write the tests, or just tell me it did? Did it set &lt;code&gt;strict: false&lt;/code&gt; somewhere to make a red squiggle go away?&lt;/p&gt;

&lt;p&gt;Generating the code was basically free. The watching wasn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So I built fe-rail: a Claude Code plugin that runs my frontend work through a fixed spec → build → review → PR loop, blocks the moves I'm scared of, and only stops to ask me two things.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqbuxdvm49hsyact4pilj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqbuxdvm49hsyact4pilj.png" alt=" " width="800" height="187"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually wanted
&lt;/h2&gt;

&lt;p&gt;Not more autonomy. Less.&lt;/p&gt;

&lt;p&gt;Most of the agent tooling I looked at was trying to give the model more room to run. I wanted the opposite. I already have a process I trust when I write frontend by hand: write a short spec, build the types first, review for the things that actually bite (types, performance, accessibility, plain code quality), then open a clean PR. I just wanted the agent to follow it every time, without me hovering over its shoulder.&lt;/p&gt;

&lt;p&gt;The other half was fear. Some mistakes are cheap to undo. A force push over a colleague's branch, or a committed &lt;code&gt;.env&lt;/code&gt;, is not. I wanted those to be impossible, not just discouraged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a run looks like
&lt;/h2&gt;

&lt;p&gt;One command, two questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;claude
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /fe-rail:fe-spec

&lt;span class="o"&gt;[&lt;/span&gt;fe-spec] Analyzing requirements... &lt;span class="o"&gt;(&lt;/span&gt;fe-analyst, fe-architect&lt;span class="o"&gt;)&lt;/span&gt;
✔ feature.md generated — 7 sections, 3 open questions resolved

Next step?
  ❯ Full auto &lt;span class="o"&gt;(&lt;/span&gt;recommended&lt;span class="o"&gt;)&lt;/span&gt; — hand off to fe-start automatically
    Build only — I&lt;span class="s1"&gt;'ll review the spec first
    Revise spec

&amp;gt; Full auto

[fe-start] Phase 2 — implementing types → hooks → components → tests
✔ 12 files created, tsc clean, lint clean, 8 tests passing

Commit and open a PR?
  ❯ Yes — split by type (feat/fix/test) and open a draft PR
    No — leave changes uncommitted

&amp;gt; Yes

✔ 2 commits created
✔ Pushed to feat/product-search-autocomplete
✔ Draft PR: https://github.com/you/your-app/pull/42
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That is the whole shape of it. Two human check-ins, "Implement?" and "Commit?". Everything between them runs on its own.&lt;/p&gt;

&lt;p&gt;Here is roughly what moved off my plate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;With fe-rail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Re-reading every diff for a stray &lt;code&gt;git add .&lt;/code&gt; or force push&lt;/td&gt;
&lt;td&gt;Blocked at the hook, before it runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Did it actually write tests?"&lt;/td&gt;
&lt;td&gt;The build ends in tests; the quality gate re-checks changed files on stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catching generic gradient-and-shadow UI by eye&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;design-nudge&lt;/code&gt; warns, or a &lt;code&gt;DESIGN.md&lt;/code&gt; sets the rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Splitting commits and writing the PR body myself&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fe-git-operator&lt;/code&gt; and &lt;code&gt;fe-pr-author&lt;/code&gt; do it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;fe-rail is five slash commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/fe-rail:fe-spec&lt;/code&gt; turns a rough request into a structured spec, then asks whether to keep going.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/fe-rail:fe-build&lt;/code&gt; implements it in a fixed order: types, then logic, then components, then tests.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/fe-rail:fe-review&lt;/code&gt; runs a four-axis review: types, performance, a11y, and quality.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/fe-rail:fe-start feature.md&lt;/code&gt; chains all of that through to a PR.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/fe-rail:fe-doc-sync&lt;/code&gt; reads your project and suggests updates to its &lt;code&gt;CLAUDE.md&lt;/code&gt; and &lt;code&gt;README&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run them one at a time when you want to drive, or let &lt;code&gt;fe-start&lt;/code&gt; take the wheel.&lt;/p&gt;

&lt;p&gt;The spec step is not limited to text. If the feature lives in a Figma file or a screenshot, &lt;code&gt;fe-vision&lt;/code&gt; reads it into concrete screens and states. If it arrives as a slide deck, &lt;code&gt;fe-deck-reader&lt;/code&gt; breaks it into screens and the flows between them. Later, at review time, &lt;code&gt;fe-vision&lt;/code&gt; can also compare a screenshot of what got built against the original reference and flag where the two drift apart. That one still surprises me when it works.&lt;/p&gt;

&lt;p&gt;There is a small detail I care about in how the two check-ins stay at two. Picking "Full auto" at the spec gate counts as the "Implement?" yes, so &lt;code&gt;fe-start&lt;/code&gt; only has to ask "Commit?" later on. I did not want the convenience of a one-command run to quietly cost me a checkpoint.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part I actually care about: hooks
&lt;/h2&gt;

&lt;p&gt;Speed was never the hard part. Trust was. So the piece I spent the most time on is the hook layer, and it runs on one rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Block dangers. Warn on quality.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Blocking means the tool call never happens (the hook exits with code 2). Warning means it happens and I get a note on stderr. A few of the blockers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;guard.sh&lt;/code&gt; stops &lt;code&gt;git add .&lt;/code&gt;, force pushes, &lt;code&gt;--no-verify&lt;/code&gt;, &lt;code&gt;git reset --hard&lt;/code&gt;, &lt;code&gt;git checkout/restore .&lt;/code&gt;, and &lt;code&gt;rm -rf /&lt;/code&gt; before they run.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;write-guard.sh&lt;/code&gt; refuses to create or edit &lt;code&gt;.env&lt;/code&gt; files, private keys, and credential dumps. It still lets through the source files you would expect, like &lt;code&gt;.env.example&lt;/code&gt; or a &lt;code&gt;CredentialForm.tsx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;config-protection.sh&lt;/code&gt; blocks edits that weaken your setup. The agent cannot flip &lt;code&gt;strict: false&lt;/code&gt;, drop in a &lt;code&gt;@ts-nocheck&lt;/code&gt;, or switch a linter's recommended rules off to make the errors disappear.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The warnings are lighter. &lt;code&gt;design-nudge.sh&lt;/code&gt; pings me when an edit reaches for the default AI look: heavy arbitrary shadows, the obligatory indigo gradient. &lt;code&gt;quality-gate.sh&lt;/code&gt; runs the linter and type check on changed files when the session ends and prints what it finds.&lt;/p&gt;

&lt;p&gt;There is an escape hatch, because there has to be one. You can turn the whole thing down with &lt;code&gt;FE_RAIL_HOOK_PROFILE=minimal&lt;/code&gt;, or switch off individual hooks by name with &lt;code&gt;FE_RAIL_DISABLED_HOOKS&lt;/code&gt;. But &lt;code&gt;minimal&lt;/code&gt; does not touch the safety blockers. If you want those gone, you have to name them one by one. Quality is negotiable. Overwriting your &lt;code&gt;.env&lt;/code&gt; is not.&lt;/p&gt;
&lt;h2&gt;
  
  
  Multiple agents, on purpose
&lt;/h2&gt;

&lt;p&gt;Each phase hands off to a small sub-agent in its own context instead of stuffing everything into one long session. Requirements analysis goes to &lt;code&gt;fe-analyst&lt;/code&gt;. Architecture questions go to &lt;code&gt;fe-architect&lt;/code&gt;. The review fans out to &lt;code&gt;fe-reviewer&lt;/code&gt;, plus a dedicated &lt;code&gt;fe-a11y-auditor&lt;/code&gt; and &lt;code&gt;fe-perf-auditor&lt;/code&gt;. The main thread stays readable because the noisy work happens off to the side.&lt;/p&gt;

&lt;p&gt;One choice I still go back and forth on: the model tiers are aliases, not pinned versions. High-judgment agents ask for &lt;code&gt;opus&lt;/code&gt;, cheap exploration runs on &lt;code&gt;haiku&lt;/code&gt;, and the rest use &lt;code&gt;sonnet&lt;/code&gt;. When a new tier ships, the agents pick it up on their own. Free upgrades, sure. It also means the same plugin version can behave a little differently a month from now, which is a strange thing to sign up for.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;That aliasing decision is what forced the most useful part of the project: a regression harness. &lt;code&gt;eval/run.sh&lt;/code&gt; checks the hooks, the profile toggles, and the plugin's own config without calling a live model, so I find out when a model update or a config change quietly breaks a guard. It exits non-zero on failure, so CI can gate on it.&lt;/p&gt;

&lt;p&gt;The design guard taught me something too. &lt;code&gt;design-nudge&lt;/code&gt; nagged me constantly at first, until I added a &lt;code&gt;DESIGN.md&lt;/code&gt; to the project. That is the actual design: once a project states its own rules, the generic nudging goes quiet and the reviewer enforces your rules instead of my defaults.&lt;/p&gt;

&lt;p&gt;I will be honest about the limits. It is opinionated: TypeScript in strict mode, Next.js App Router or a Vite SPA, Tailwind, shadcn/ui. Off that stack, most of the value drains away. It is young (v1.10 as I write this), and it is frontend only by design. It is really just my own frontend process, written down and made hard to skip.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you are on Claude Code and roughly that stack:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add sh5623/fe-rail
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;fe-rail@fe-rail-market
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two things are worth doing right after install. Give the project a &lt;code&gt;CLAUDE.md&lt;/code&gt; (run &lt;code&gt;/init&lt;/code&gt;) so the agents are not reasoning blind about your stack. And allow &lt;code&gt;Bash(git *)&lt;/code&gt; and &lt;code&gt;Bash(gh pr *)&lt;/code&gt; in your settings so PR creation does not prompt you at every step.&lt;/p&gt;

&lt;p&gt;It is open source under MIT:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/sh5623" rel="noopener noreferrer"&gt;
        sh5623
      &lt;/a&gt; / &lt;a href="https://github.com/sh5623/fe-rail" rel="noopener noreferrer"&gt;
        fe-rail
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Claude Code plugin — spec → build → review → PR harness for Next.js, Vite SPA &amp;amp; TypeScript (Tailwind, shadcn/ui)
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;fe-rail&lt;/h1&gt;
&lt;/div&gt;

&lt;div&gt;
  &lt;a href="https://github.com/sh5623/fe-rail/README.ko.md" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/95130dfd7592caa9e30a2e7fc91b815add3b95dcf6da95559e5c656216a2d3f9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c616e672ded959ceab5adec96b42d6c69676874677265793f7374796c653d666c61742d737175617265" alt="한국어"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/sh5623/fe-rail/README.md" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/2c10b7e89ab0f84c58cda2d5ee1b0eeab1a0f39439a9d7c7ff787cae7d6dce19/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c616e672d456e676c6973682d626c75653f7374796c653d666c61742d737175617265" alt="English"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/sh5623/fe-rail/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/422db9fd40f5831c765cf6530b6750c081b696bd18d904cf89554df98c676277/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e3f7374796c653d666c61742d737175617265" alt="MIT License"&gt;&lt;/a&gt;
&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Frontend-focused Claude Code plugin
Automated spec → build → review → PR workflow for Next.js App Router / Vite SPA (TanStack Router · React Router 7) + TypeScript, with full Tailwind v3/v4 / shadcn/ui support.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Installation&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;claude

/plugin marketplace add sh5623/fe-rail
/plugin install fe-rail@fe-rail-market&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/sh5623/fe-rail/docs/assets/workflow.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fsh5623%2Ffe-rail%2FHEAD%2Fdocs%2Fassets%2Fworkflow.svg" alt="fe-rail workflow: spec, build, review, PR"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;

&lt;/div&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;$ claude
&amp;gt; /fe-rail:fe-spec

[fe-spec] Analyzing requirements... (fe-analyst, fe-architect)
✔ feature.md generated — 7 sections, 3 open questions resolved

Next step?
  ❯ Full auto (recommended) — hand off to fe-start automatically
    Build only — I'll review the spec first
    Revise spec

&amp;gt; Full auto

[fe-start] Phase 2 — implementing types → hooks → components → tests
✔ 12 files created, tsc clean, lint clean, 8 tests passing

Commit and open a PR?
  ❯ Yes — split by type (feat/fix/test) and open a draft PR
    No — leave changes uncommitted

&amp;gt; Yes

✔ 2 commits created
✔ Pushed to feat/product-search-autocomplete
✔ Draft PR: https://github.com/you/your-app/pull/42
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Two…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/sh5623/fe-rail" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;I would like to know where it breaks for you. What is the one thing your AI coding setup does that you wish it would ask you about first?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>frontend</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I kept re-explaining my stack to Claude on every new project — so I built it one</title>
      <dc:creator>SeungHo Lee</dc:creator>
      <pubDate>Fri, 03 Jul 2026 14:46:28 +0000</pubDate>
      <link>https://dev.to/dev_sh/i-kept-re-explaining-my-stack-to-claude-on-every-new-project-so-i-built-it-one-iie</link>
      <guid>https://dev.to/dev_sh/i-kept-re-explaining-my-stack-to-claude-on-every-new-project-so-i-built-it-one-iie</guid>
      <description>&lt;p&gt;fe-rail solved the workflow problem. Spec, build, review, PR. The agent stopped skipping steps, stopped committing &lt;code&gt;.env&lt;/code&gt; files, stopped needing me to catch the same three mistakes every session. That part I wrote about already.&lt;/p&gt;

&lt;p&gt;What I didn't fix was the first hour of every new project. Clone an empty repo, open Claude Code, and watch it ask me the same twenty questions it asked last time. Router or no router? Where does server state live? What's the button supposed to look like when it's loading? I'd answer them once, in prose, and by the third project I was pasting the same paragraph into the same first prompt. That's not a workflow problem. That's a missing-scaffolding problem, and no amount of process discipline fixes it. You can review a PR perfectly and still end up with a design system the agent invented from nothing, because nothing told it not to.&lt;/p&gt;

&lt;p&gt;So I built railhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I actually wanted
&lt;/h3&gt;

&lt;p&gt;Not another SPA boilerplate. There are plenty of those, and none of them were built with a specific failure mode in mind: an AI agent filling gaps with its training-data defaults instead of my decisions. Left alone, Claude's default frontend looks like every other AI-generated frontend: purple-to-blue gradients, Inter everywhere, a grid of identical icon-plus-heading cards, a cream background nobody asked for. It's not wrong, exactly. It's just not mine, and it's the same "not mine" every single time.&lt;/p&gt;

&lt;p&gt;I wanted a repo where the agent's first move is reading three files, not guessing. Where "minimize hallucination" isn't a vibe I repeat in the system prompt but an actual constraint the codebase enforces: API types come from one generated file or the build fails, not from whatever shape the agent thinks the backend probably returns.&lt;/p&gt;

&lt;p&gt;There are exactly two rules at the top of &lt;code&gt;AGENTS.md&lt;/code&gt;, and every other decision in the template traces back to one of them. First: minimize hallucination. One API client, one set of generated types, no inventing an endpoint that sounds plausible. Second: no over-engineering. Don't add an abstraction, a library, or a folder nobody asked for, even if it's a "best practice" the model has seen a thousand times in training data. That second rule is the one I almost skipped, and it's the one that matters more day to day. An agent that hallucinates an endpoint gets caught by &lt;code&gt;tsc&lt;/code&gt;. An agent that quietly adds a state management library nobody needed just makes the codebase worse in a way nothing catches.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's actually in it
&lt;/h3&gt;

&lt;p&gt;React 19, TypeScript 6 in strict mode, Vite 8. React Router 7 in library mode: routing only, no data loaders fighting with the query cache. TanStack Query owns every byte of server state; Zustand only shows up for state that's genuinely shared across the tree, not as a reflex. React Hook Form plus Zod for anything with an input field. Tailwind v4 and shadcn/ui, added through the CLI so I own the component after it lands, not before. Biome instead of the ESLint-plus-Prettier stack, because two tools that can disagree with each other is one tool too many for an agent to reconcile on its own. Vitest for units, Playwright for the two paths that actually matter: the happy one and the one where the request fails.&lt;/p&gt;

&lt;p&gt;None of that is exotic. What's different is what backs it up.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a first run looks like
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://github.com/sh5623/railhead.git
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;railhead
&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm dev

  VITE v8.0.16  ready &lt;span class="k"&gt;in &lt;/span&gt;340 ms
  ➜  Local:   http://localhost:5173/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open that URL without touching &lt;code&gt;.env&lt;/code&gt; first and you get a blank white page, on purpose. &lt;code&gt;src/lib/env.ts&lt;/code&gt; validates &lt;code&gt;VITE_API_BASE_URL&lt;/code&gt; with Zod at startup and throws if it's missing. Loudly, in the console, instead of quietly rendering a broken app that looks fine until you click something. I didn't want the failure mode to be "works until it doesn't." I wanted it to be "doesn't work, and tells you exactly why, in one line."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the home route is the onboarding walkthrough I mentioned, not a placeholder, but an actual page explaining the rules it's about to hand off to. Ask an agent to add a screen after that, and it's not starting from a blank file and a vague memory of what React apps usually look like. It's starting from a &lt;code&gt;LoginPage&lt;/code&gt; and a &lt;code&gt;HealthBadge&lt;/code&gt; it can read, an &lt;code&gt;AGENTS.md&lt;/code&gt; it can grep, and a &lt;code&gt;pnpm check &amp;amp;&amp;amp; pnpm typecheck&lt;/code&gt; it knows will fail loudly if it drifts.&lt;/p&gt;

&lt;p&gt;That last part isn't optional either. Husky runs &lt;code&gt;lint-staged&lt;/code&gt; on every commit. CI runs the full sequence on every push, in a clean environment: typecheck, &lt;code&gt;biome ci&lt;/code&gt;, unit tests, build, then Playwright. "Works on my machine" and "works in the PR" can't quietly diverge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three files, on purpose
&lt;/h3&gt;

&lt;p&gt;Here's the thing I kept getting wrong in earlier attempts: I'd write one long README explaining everything, and the agent would either skim it or treat conventions and vibes as the same category of instruction. So railhead splits on a rule I stole from the tooling itself: &lt;strong&gt;if a tool can enforce it, the docs don't repeat it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;biome.json&lt;/code&gt; and &lt;code&gt;tsconfig.json&lt;/code&gt; cover lint, format, import order, null-safety. Run &lt;code&gt;pnpm check&lt;/code&gt; and &lt;code&gt;pnpm typecheck&lt;/code&gt; and that whole category is handled. No prose required, because prose is the thing agents forget under pressure.&lt;/p&gt;

&lt;p&gt;What's left goes in &lt;code&gt;AGENTS.md&lt;/code&gt;, and only what's left: the single API pattern (one generated &lt;code&gt;openapi-fetch&lt;/code&gt; client, no hand-rolled &lt;code&gt;fetch&lt;/code&gt; calls, no &lt;code&gt;as any&lt;/code&gt; when the types don't match: if they don't match, the spec is stale, go fix the spec), where server state is allowed to live, how query keys get built so cache invalidation doesn't turn into guesswork. &lt;code&gt;CLAUDE.md&lt;/code&gt; is a two-line stub that just points at &lt;code&gt;AGENTS.md&lt;/code&gt;, so there's exactly one document to keep in sync, not two drifting copies.&lt;/p&gt;

&lt;p&gt;Then there's &lt;code&gt;DESIGN.md&lt;/code&gt; and &lt;code&gt;PRODUCT.md&lt;/code&gt;, which exist because "make it look good" is not an instruction, it's a wish. &lt;code&gt;DESIGN.md&lt;/code&gt; has actual tokens: OKLCH color, a five-step elevation ladder from flat to overlay, a rule that says the brand accent gets 4 to 8 percent of any surface and nothing more. It also has a bans list, and I'll admit I enjoyed writing it more than I should have: no gradient text, no glassmorphism by default, no identical card grids, no purple-to-blue gradient as the default accent (calling that one out by name, since I've shipped it myself). If you're about to reach for one of these, the file tells you to restructure instead. &lt;code&gt;PRODUCT.md&lt;/code&gt; is shorter: who the user is, three words for the brand voice, and a short list of things this product is explicitly not trying to be.&lt;/p&gt;

&lt;p&gt;Three files. Not one giant one, not ten scattered ones. Enough separation that an agent reading "add a settings page" pulls the design tokens without also re-reading the API conventions it already has memorized for this session.&lt;/p&gt;

&lt;p&gt;The tokens are OKLCH, not hex, because I wanted contrast ratios to be something you can reason about instead of eyeball, and the elevation ladder is mapped directly to how shadcn components already render, so a new component lands on-system without anyone deciding where its shadow goes by hand. The accent cap is the part I'd point to first, though: 4 to 8 percent of any surface, nothing more. That's the actual mechanism behind banning that purple gradient I keep bringing up. It's not banned for being ugly. It's banned because "gradient as the whole surface" and "accent as 4 percent of the surface" are two different design languages, and only one of them was a decision I made on purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  The onboarding page is the demo
&lt;/h3&gt;

&lt;p&gt;I didn't want to just tell people this works, so the template ships with a page that shows it. Clone the repo, run &lt;code&gt;pnpm dev&lt;/code&gt;, and the home route isn't a blank canvas. It's a scrolling walkthrough of the template itself, station by station, built along a little train-line metaphor because I couldn't resist the name. It covers the two hard rules, walks through the canonical patterns, and has a section titled, honestly, "installing fe-rail is optional." Because it is. railhead decides what you build with. fe-rail decides what order you build it in. They're not the same rail, and neither one requires the other.&lt;/p&gt;

&lt;p&gt;Two of the pages in that walkthrough aren't demo content, though. They're load-bearing. &lt;code&gt;src/features/auth&lt;/code&gt; and &lt;code&gt;src/features/health&lt;/code&gt; are the canonical implementations of "here's the one way we call the backend" and "here's the one way we gate a route." The README is explicit that when you bootstrap a real project from this template, you delete the onboarding tour and keep those two. They're not sample code to be replaced. They're the pattern the rest of the app is supposed to copy.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'm still not sure about
&lt;/h3&gt;

&lt;p&gt;I'll be honest about where this is thin. railhead is young. I started building it today, and as of writing it hasn't been bootstrapped into an actual production project yet, just built out and walked through by hand. Everything above is real and it works, but "works when I test it" and "survives someone else's first messy feature branch" are different claims, and I only get to make the first one right now.&lt;/p&gt;

&lt;p&gt;The thing I still go back and forth on is the shadcn MCP server. It's committed to the repo, on purpose, so the agent can look up a component's real props instead of guessing. That's the whole "minimize hallucination" principle applied to the UI layer, not just the API layer. But it also means the first thing a new contributor sees is a permission prompt, and I'm not fully sold that the tradeoff is worth it for every team that clones this. Right now I'm leaving it in and living with the friction.&lt;/p&gt;

&lt;p&gt;The other open question is how much of &lt;code&gt;DESIGN.md&lt;/code&gt; survives contact with an actual designer who wants their own system instead of mine. I wrote the bans list from my own taste, and taste doesn't generalize. I think the structure — tokens plus a bans list plus a reason for each ban — generalizes better than the specific values do, but I haven't tested that claim on anyone else's project yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/sh5623/railhead.git
&lt;span class="nb"&gt;cd &lt;/span&gt;railhead
pnpm &lt;span class="nb"&gt;install
cp&lt;/span&gt; .env.example .env
pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's MIT licensed, same as fe-rail. If you're already using fe-rail, they're built to sit together: railhead gives the agent the stack and the rules, fe-rail gives it the workflow. Neither one asks you to adopt the other.&lt;/p&gt;

&lt;p&gt;I'd like to know what breaks first when you try it on your own stack. What's the one convention your team has that isn't in &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;DESIGN.md&lt;/code&gt;, or &lt;code&gt;PRODUCT.md&lt;/code&gt; yet? Would you rather the agent inferred it, or asked?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>frontend</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
