Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Spec-driven Claude Code setup

A drop-in Claude Code configuration for building features spec-first: you think it through, write down what the system should do, plan against that, and only then write code. Four slash commands, three subagents, and one hook that keeps the whole thing honest.

It is language- and domain-agnostic. Copy it into any project, adapt one file, and go.

Read the first half to understand it. The second half is setup.

Understand it
Executive summary why it's shaped this way
The layout rules vs notes, and why it's two directories
The cycle the stages, and what each is forbidden to do
The spec guard the hook, and when to switch it off
The learning track closing the gaps AI-assisted work leaves
What's in the box file by file
Set it up
Install copy it, edit one file
Adapting it what's load-bearing, what isn't

Executive summary

This is not three commands you run in order. You could call them in sequence and get almost none of the value. What makes it work is what each stage is forbidden to do — the fences, not the steps.

Everything follows from one conviction: /specs is the source of truth for intended behavior, and it must never change by accident. Code drifting from a spec is a bug you can find. A spec quietly edited to match the code is a bug you can't.

Five constraints hold it up:

Two roots: rules and notes. There are exactly two places writing lands. specs/ holds rules — what the system is supposed to do. docs/ holds notes — everything else. Only one directory in the project can tell you what is supposed to be true, and only that directory is defended. See The layout for why this one line does so much work.

Each stage is fenced into one question. /draft-spec decides what the system should do and is forbidden from saying how. /spec-driven-feature decides how to build it and is forbidden from changing what. /outline-feature decides nothing at all. Each also owns exactly one directory and cannot write to another's. No stage can rewrite the ground it's standing on.

Fresh session per stage — that's the mechanism, not hygiene. Each stage starts cold and re-derives what it needs through the inspectors. A stage that inherited the last stage's reasoning would inherit its mistakes too, and "I already decided that" is exactly the thought you don't want carried forward.

Read-only inspectors. spec-inspector (reads /specs only) and code-inspector (reads everything but /specs) get Read, Grep, Glob and nothing else — they physically cannot edit. They burn tokens searching and hand back a distilled summary, keeping the main session's context for the work. The split is deliberate: intent and implementation are read by different agents so they can be compared rather than blurred.

Drift is surfaced, never resolved. When spec and code disagree, that becomes a decision for you — /spec-driven-feature stops and shows a fixed menu. The model's job is to notice; the ruling is yours. The same rule binds every subagent: a tutor that spots a discrepancy reports it and stops.

Running underneath all of it: the model surfaces, the human decides. The hook is that principle in executable form — and, as the cycle shows, it's the last line of defence rather than the first.


The layout

your-project/
├── specs/                        ← RULES. what the system is supposed to do.
│   └── <slug>.md                   guarded: writing here always asks first.
│
├── docs/                         ← NOTES. everything else. never authoritative.
│   ├── drafts/<slug>-outline.md    a half-formed idea, before it's settled
│   ├── plans/<slug>-plan.md        how we intend to build it, this once
│   └── learning/                   explainers for concepts you flagged
│       ├── INDEX.md                  what's been written (permanent record)
│       ├── QUEUE.md                  what's still owed you (drained, not kept)
│       └── <concept>.md              the explainer itself
│
├── scripts/lint.sh               ← EDIT ME. your one validation command.
└── .claude/                        the commands, agents, and hook

Rules vs notes, in plain English

The technical words are normative and descriptive. They're worth knowing, but here's what they actually mean:

  • A rule (normative) says what SHOULD be true. If the world disagrees with it, the world is wrong. A law says don't park here; a car parked here doesn't make the law out of date, it makes the car wrong.
  • A note (descriptive) says what IS true, or what someone thought at the time. If the world disagrees with it, the note is wrong. A photo of an empty parking space doesn't stop anyone parking there — it just goes stale.

specs/ is rules. docs/ is notes. That's the whole distinction.

Why it's worth a whole directory

Because it's what lets something be wrong.

Say the spec says "an expired draft returns 410" and the code returns 500. One of them has to give. Because the spec is a rule, the answer is immediate: the code is wrong, go fix the code.

Now suppose specs were just documentation — notes, like everything else. The same mismatch has a much more tempting answer: update the doc to say 500. Nothing looks broken afterwards. The tests pass, the docs match the code, and the bug is now the specification. Do that a few times and you no longer have a spec at all — you have a slowly-written description of whatever the code happened to do, which can never disagree with anything, which means it can never catch anything.

A source of truth only works if it can contradict you. The moment it's allowed to quietly re-describe reality, it stops being a source of truth. That is the one failure this whole setup is built to prevent, and it's why specs/ is a separate directory with a hook in front of it rather than a folder inside docs/.

Why drafts, plans, and learning are all notes

  • An outline is thinking out loud. It literally says Status: OUTLINE (not yet a spec). Nothing should be held to it.
  • A plan is how you intend to build the thing, once. The moment it's implemented it's history — useful to look back at, but it stopped describing the future.
  • Learning docs explain concepts. They're written to be understood, which means they simplify, and a simplification must never become the thing you're held to. concept-tutor is told this directly: descriptive, never normative. If it notices code contradicting a spec, it reports it and stops.

None of these can tell you what the system should do. So none of them live in the one directory that can.


The cycle

Four stages plus a learning track. Each stage runs in a fresh session — every command ends by telling you to start a new one.

  idea in your head
        │
        ▼
  /outline-feature "<one-line idea>"        ← fast, ≤3 questions, no interview
        │  writes docs/drafts/<slug>-outline.md
        │  "Open questions" = the agenda for the next stage
        ▼   ─────────── fresh session ───────────
  /draft-spec docs/drafts/<slug>-outline.md    ← the thinking stage
        │  Phase A: one-topic-at-a-time interview, pushes on edge cases
        │           + asks "familiar with this?" as concepts come up
        │  Phase B: only after you confirm the summary
        │  writes specs/<slug>.md   ·  criteria must be CHECKABLE
        │  files gaps → docs/learning/QUEUE.md
        ▼   ─────────── fresh session ───────────
  /spec-driven-feature "<request>"          ← discovery + planning, zero code
        │  1. inspect /specs   (spec-inspector)  ← always FIRST
        │  2. inspect code     (code-inspector)  ← only after specs
        │  3. drift? → "Decision needed" menu → you rule, BEFORE planning
        │  writes docs/plans/<slug>-plan.md
        │  files gaps → docs/learning/QUEUE.md
        ▼   ─────────── fresh session ───────────
  implement (plan mode recommended)
        │  ./scripts/lint.sh --fix
        │  post-implementation /specs review ← baked into every plan
        ▼
  /explain-concepts                         ← no args: drains the queue
           → concept-tutor agents (parallel) → docs/learning/

Which one am I reaching for?

The three commands look similar — they all "write something down about a feature before you build it" — but they answer three different questions, and each is forbidden from answering the others':

The question it answers Not allowed to
/outline-feature "don't let me forget this" decide anything
/draft-spec WHAT should the system do? say how — "do not write or plan implementation code"
/spec-driven-feature HOW do we build it? say what — "do not edit /specs"

Outline decides nothing. /draft-spec decides WHAT. /spec-driven-feature decides HOW. Those quotes are real hard rules in the command files, so the boundary isn't a convention you have to remember — each stage is fenced into its own question.

It's the same rules-vs-notes line from the layout: what should be true is a rule and lives in specs/ forever; how you intend to make it true is a note that dies once it's built.

/outline-feature is a capture tool, not a thinking tool. Use it when an idea arrives while you're busy with something else and you want it out of your head without leaving what you're doing. That's why it asks at most three questions: a capture tool that interviews you has already failed — it pulled you out of the thing you were protecting. Two minutes, then back to work. The outline waits indefinitely; pick it up in a week and /draft-spec reads it as the agenda.

If you already know the idea is worth real thought and you have the hour, skip it and go straight to /draft-spec. The outline stage exists for the times you don't have the hour.

Where the guard fires — and where it doesn't

Reading the cycle, it's easy to assume the hook is what keeps specs safe. It isn't — it's the backstop. It stays silent almost everywhere:

  /outline-feature       writes docs/drafts/    → silent   (not a spec)
  /draft-spec            writes specs/          → ASKS ①   (expected)
  /spec-driven-feature   writes docs/plans/     → silent
     └ finds drift?      shows you a menu       → silent   (nothing written yet)
  implement              writes src/, tests/    → silent
     └ "fixes" the spec  writes specs/          → ASKS ②   (the alarm)
  /explain-concepts      writes docs/learning/  → silent

Two moments, and they are opposites:

① During /draft-spec the prompt is a foregone conclusion — you ran the command whose entire job is writing a spec. It tells you nothing you didn't already know. That's what ALLOW_SPEC_EDIT is for.

② During implementation the identical prompt is an alarm: why is something touching the source of truth while fixing a test? That's the one it exists for.

Two layers, and the hook is the second

This is the part that isn't obvious from the diagram.

Layer 1 — the commands ask you. /spec-driven-feature is told "do not edit /specs", "do not silently update /specs", and step 4 stops dead with a Decision menu the moment code and spec disagree:

## Decision needed
I found a mismatch: the spec requires 410 for expired drafts; the code returns 500.
Please choose:
1. Refine the request
2. Update /specs (run /draft-spec)
3. Follow /specs and change the code
4. Treat /specs as outdated
...

Notice the hook is not involved. Nothing is being written — there's nothing to intercept. This conversation happens because the command says so.

Layer 2 — the hook fires. Only when Layer 1 fails: the model ignores its instructions and edits a spec anyway.

In a well-behaved run the hook never makes a sound. That is the design working, not the guard being useless. Instructions are a request — a model can drift past them, especially deep in a long session. A hook is a wall. You want both, because the layer that catches the normal case and the layer that catches the failure case are not the same layer.

But we still use plan mode at the end — so what was the plan file for?

Fair question; they are genuinely different things.

The plan file is a decision, made in its own session with fresh context, after the inspectors have read the specs and the code. You can read it the next morning, argue with it, or hand it to someone else. It survives.

Plan mode is the execution pass, and it happens later — maybe days later, against code that has moved since. It re-grounds the decision in today's reality and turns it into the concrete edit list: the plan file says "expired drafts must return 410, handle it in the expiry check"; plan mode says "here are the six edits I'm about to make, in order, to the code as it exists right now."

Decision, then execution. If the plan file is fresh and the change is small, skip plan mode — it's a recommendation, not a rule.

Where you're in the loop

Five checkpoints, all deliberate: the interview summary, any spec/code conflict, the plan itself, the concept list, and any prompt from the spec guard. The rest runs without you.


The spec guard

guard-specs.sh is a PreToolUse hook, registered in .claude/settings.json. Any write touching specs/ returns permissionDecision: "ask" — you get the normal permission prompt, with a reason explaining why. Everything else passes silently.

It covers both ways a file gets written:

  • the edit tools (Write, Edit, NotebookEdit), by checking the target path
  • the Bash tool, by scanning the command text — otherwise cat > specs/x.md would walk straight past the guard

Reading a spec never prompts. The inspectors do it constantly, and a guard that nags on reads is a guard people switch off.

The Bash scan is a heuristic — shell can't be parsed with a regex — and it's tuned to over-match rather than under-match. That's affordable precisely because the outcome is a prompt, not a refusal: a false positive costs one keystroke, a false negative costs the guard its whole reason to exist.

If jq is missing the hook denies rather than failing open, and says so.

Turning it off on purpose

ALLOW_SPEC_EDIT=1 claude

Switches the guard off for one session. This isn't about saving keystrokes/draft-spec writes about one file, so it's one prompt. It's about keeping the prompt meaningful.

A prompt is only worth anything if it might surprise you. During /draft-spec a spec-write prompt is guaranteed, so it carries no information — and prompts that carry no information don't sit there harmlessly. They train you to click through. Do that a few times and the day the alarm fires — a spec write during a bugfix — you approve it on autopilot too.

Switch the guard off when it can't tell you anything, so it still means something when it speaks.

Situation Flag Why
Writing a new spec (/draft-spec) ON Spec-writing is the job. The prompt is a foregone conclusion.
Reworking / splitting specs ON Same, times twenty.
Code diverged, and the code is right OFF One deliberate exception. The prompt is the decision.
Code diverged, and it's a bug OFF The prompt is the alarm. This is the whole point.
Any normal coding session OFF Nothing should touch specs. If something does, you want to know.
Headless / CI (claude -p) ON if it must write specs "ask" assumes a human exists. Nobody's there to answer.

Code diverged — which is it? Both look identical from the outside. One question separates them: did we learn something, or did we just fail to do the thing? The spec said fetch everything in one request, the API caps at 100 — the spec was wrong about the world, update it. The spec said 410, the code says 500 — nobody learned anything, someone slipped, fix the code. Claude can't make that call, which is exactly why it hands you a menu instead of picking.

The model cannot set this. The hook is spawned by the Claude Code process and inherits its environment, not the agent's — export ALLOW_SPEC_EDIT=1 inside a Bash call never reaches it. The switch is yours, thrown before the session starts.

Its bluntness is contained by the ritual: the flag lasts one session, and a stage is one session. Launch it for /draft-spec, get your spec, close it. The implementation session is a new process with the guard back up — not because you remembered, but because that's how the workflow already tells you to work.

Don't export it in your shell profile. That silently disarms the guard for every session in that terminal: no protection and no prompt telling you so.


The learning track

Optional, and the part most worth understanding — it's what makes this useful for AI-assisted work specifically. When you build with an AI, features land that lean on ideas you never internalized. You end up with working code you can't fully judge. This track closes that gap.

The trick is when it captures. During the /draft-spec interview, whenever a term of art comes up, you get one question:

"That means the estimate has to be a range rather than a single number — that's quantile regression. Familiar with it, or want it explained later?"

One word, no detour. /spec-driven-feature does the same for concepts the plan introduces — the spec says "collapse duplicates", the plan picks union-find, and that's a choice the interview couldn't have anticipated.

Capturing at the moment of introduction beats guessing later, and it's what makes this work in any domain: "are you familiar with this?" doesn't care whether the answer is quantile regression or Raft.

Flagged concepts land in docs/learning/QUEUE.md. Later, /explain-concepts with no arguments drains it: spawns a concept-tutor per concept, which writes a 2–4 page plain-language explainer anchored in your code, plus a runnable notebook or script where watching values change beats prose.

Two deliberate details:

  • The spec gets a ## Concepts section, but with no familiarity markers — just "this feature relies on X, because Y". That's permanent and useful to any reader. Whether you knew it last Tuesday isn't intended behavior and has no business in the source of truth. That transient fact lives in the queue, which is drainable by design.
  • INDEX.md is the permanent record; queue entries are deleted once written, so the queue stays a to-do list rather than a second, staler index.

Don't want it? Delete explain-concepts.md and concept-tutor.md. Nothing else depends on them.


What's in the box

File What it does
.claude/commands/outline-feature.md Capture a half-formed idea into docs/drafts/. ≤3 questions.
.claude/commands/draft-spec.md Interview, then write a spec with checkable criteria.
.claude/commands/spec-driven-feature.md Inspect specs → inspect code → write a plan. Never code.
.claude/commands/explain-concepts.md Drain the learning queue into explainers.
.claude/agents/spec-inspector.md Read-only over /specs. Also judges "ready to build?"
.claude/agents/code-inspector.md Read-only over everything else. Finds drift.
.claude/agents/concept-tutor.md Writes the explainers. Long, plain, anchored in your code.
.claude/hooks/guard-specs.sh Prompts on any write to specs/.
.claude/settings.json Registers the hook.
scripts/lint.sh Edit this. Your project's one validation command.


Setup

Install

# from your project root
cp -r /path/to/ai_coding_setup/.claude .
cp -r /path/to/ai_coding_setup/scripts .    # if you don't already have scripts/

# the guard needs jq
brew install jq        # or: apt install jq

Then do the one thing you must do:

Edit scripts/lint.sh. It ships as a placeholder that exits 1. Replace it with your project's real checks — linters, type checkers, tests, whatever "this code is done" means for you. Examples for Python/Node/Go/Rust are commented in the file.

That indirection is the whole generalization strategy: the commands and agents only ever know ./scripts/lint.sh --fix, so they never need to learn your stack.

The directories (specs/, docs/drafts/, docs/plans/, docs/learning/) get created as you go. Restart Claude Code so it picks up settings.json, and run /outline-feature to check the commands are live.

Adapting it

scripts/lint.sh is the only required change. Everything else works as-is.

Optionally, tell Claude where things are — add to your CLAUDE.md:

## Spec workflow
- `/specs` is the source of truth for intended behavior. Nothing else goes there.
- Outlines live in `docs/drafts/`, plans in `docs/plans/`, learning materials in
  `docs/learning/` — all descriptive, none authoritative.
- `./scripts/lint.sh --fix` is the single validation command.

The conventions worth keeping if you rename things: one directory — and only one — is normative; specs read before code; one validation command; each stage owning one directory; the human ruling on drift. Those are load-bearing. The directory names aren't — if you move specs, update the case glob in guard-specs.sh to match.

Already have specs? They won't have a ## Concepts section, and nothing will be queued. /explain-concepts <spec-path> still mines a file the old way — that path exists as the fallback.

Notes

  • .claude/settings.local.json is Claude Code's per-machine permission state. It's gitignored here and shouldn't be committed or shared.
  • The hook needs jq. It's the only dependency.
  • "ask" assumes a human is present. In headless runs (claude -p), set ALLOW_SPEC_EDIT=1 or the spec writes will have nobody to answer them.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages