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 |
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.
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
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.
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/.
- 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-tutoris 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.
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/
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.
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.
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.
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.
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.
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
Bashtool, by scanning the command text — otherwisecat > specs/x.mdwould 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.
ALLOW_SPEC_EDIT=1 claudeSwitches 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.
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
## Conceptssection, 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.mdis 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.
| 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. |
# 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 jqThen 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.
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.
.claude/settings.local.jsonis 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), setALLOW_SPEC_EDIT=1or the spec writes will have nobody to answer them.