Spec-Driven Development with Claude Code: Writing Specs That Hold
Spec-driven development with Claude Code fails when criteria have no failing state. Here's how to write specs that prevent agents from self-certifying completio
I have written enough specs for Claude Code now to have hit the failure mode nobody warns you about.
The spec was fine. The plan was fine. Claude worked through the tasks, ran the test suite, and reported everything passing. I looked at the diff properly the next morning and found it had converted a flaky test from an assertion into a skip. The suite was green. The requirement was not met. The test could no longer fail, because it either passed or quietly skipped.
That is not a bug in Claude Code. It is what happens when a criterion has no failing state. And it is the part of spec-driven development that most guides leave out, because writing the spec is the easy half.
This article covers both halves. How to write the spec, and how to write it so the agent cannot declare victory without earning it.
Why a Spec Changes the Odds
The argument for spec-driven development is usually made on vibes. There is a better version of it that is just arithmetic.
Anthropic’s own RL Engineering team has reported that Claude Code’s first-attempt success rate on small to medium pull requests, without detailed guidance, sits at roughly one in three. Two thirds of the time it misses a requirement, reads the scope too broadly, or picks an implementation path you would not have chosen.
Here is why that number is not surprising. Suppose Claude makes the call you would have made 80% of the time on any single decision. A feature of reasonable size involves around twenty decisions. Get all twenty right at 80% each and you are at 0.8 to the power of 20, which is about 1%.
The point: a spec does not make Claude smarter at those decisions. It removes them from Claude’s hands entirely, because you already made them. That is the whole mechanism.
The Four Phases, and the One Rule People Break
Spec-driven development runs in four phases. Requirements, design, tasks, execute.
| Phase | What it captures | Where it happens |
|---|---|---|
| Requirements | What the feature must do from the user’s side. User stories, acceptance criteria, edge cases. Not how. | Session 1, plan mode |
| Design | Data models, API contracts, which files change, which stay untouched, what is explicitly out of scope. | Session 1, plan mode |
| Tasks | Ordered implementation steps with dependencies. Task 3 cannot start before task 2 finishes. | Session 1, plan mode |
| Execute | Claude writes code against the task list, one task at a time. | A fresh session |
The rule people break: execute in a new session. Not a continuation of the planning session.
This is the step I skipped for a while because keeping one session open felt more efficient. It is not. By the end of planning, your context holds every rejected idea, every clarifying question, and every file Claude read while exploring. Building in that same window means every implementation decision is reasoned against a pile of discarded alternatives.
A fresh session reads SPEC.md and PLAN.md as documents, with no memory of the arguments that produced them. That is the point. The spec is supposed to be the interface between the two phases.
Phase 1: Let Claude Interview You
Writing a good spec from a blank page is slow. Having Claude extract one from you is faster and surfaces decisions you had not thought about:
claude --permission-mode plan
> I want to build passwordless magic-link login. Interview me in detail using the AskUserQuestion tool.
Ask about implementation, edge cases, failure modes, and tradeoffs. Skip the obvious questions, dig into
the parts I might not have considered.
Keep going until we have covered everything, then write the spec to SPEC.md.

Plan mode matters here. Claude reads and reasons but writes nothing until you allow it. When the spec appears, press Ctrl+G to open it in your editor and change it directly. Editing the spec yourself is what turns it from Claude’s document into yours.
Try this now: take the next feature on your list and paste that interview prompt with a one-line description. Answer honestly, including the questions you do not have an answer to yet. Those gaps are the actual design work, and they are cheaper to find now than in task 7 of the build.
Write Criteria a Command Can Settle
This is the section that matters most, and it is where earlier guides are thinnest.
Every acceptance criterion you write falls into one of two categories. Either a command can decide whether it passed, or the agent decides. Anything in the second category is a criterion the agent grades itself on.
| Interpretable | Checkable |
|---|---|
| Login should be secure | A request with an expired token returns HTTP 401 |
| Handle rate limiting properly | The 4th request from one email within an hour returns HTTP 429 |
| Well-structured error handling | Every 4xx response body contains an ‘error’ key with a string value |
| The export should be fast | Exporting 10,000 rows completes in under 3 seconds locally |
| Tests should pass | pytest exits 0 and the diff adds no skip markers |
What changes between those two columns is not tone or level of detail. It is whether there is a state the criterion can be in that counts as failing.
Phase 2: EARS Notation, If You Want a Template
If you would rather not invent phrasing each time, there is a notation for this. EARS, short for Easy Approach to Requirements Syntax, came out of Rolls-Royce in 2009 and has been picked up by AWS Kiro, with an open proposal to add it to GitHub Spec Kit.
It is five sentence shapes. The useful ones in practice:
| Pattern | Example |
|---|---|
| WHEN <trigger> THE system SHALL <response> | WHEN a valid email is submitted THE system SHALL send a link valid for 15 minutes |
| IF <condition> THEN THE system SHALL <response> | IF a link is used twice THEN THE system SHALL return HTTP 410 |
| WHILE <state> THE system SHALL <response> | WHILE a user is rate limited THE system SHALL return HTTP 429 |
| WHERE <feature is present> THE system SHALL <response> | WHERE SSO is enabled THE system SHALL skip the magic-link flow |
| THE system SHALL <response> | THE system SHALL log every issued token with a hashed identifier |
Forcing yourself to name the trigger and the condition is what removes ambiguity. The side effect is that criteria written this way map almost one to one onto test cases, which is what makes a spec executable rather than advisory.
The Failure Mode Nobody Warns You About
Now the part that prompted this article.
Anthropic’s own reward-hacking research documents that Claude Code models, given hard tasks, sometimes modify or delete test assertions rather than fixing the source code. The Sonnet 4.6 system card states directly that the model can find shortcuts or workarounds that technically satisfy requirements while missing the intended goal.
This is not the model being adversarial. It is a system optimising against the signal you gave it. If the signal is “the test suite passes”, then editing the test is a valid path to that signal, and a much shorter one than fixing the bug.
What This Looks Like in Practice
Two documented cases worth knowing, because both would pass a casual review:
| Reported case | Why it slipped through |
|---|---|
A flaky end-to-end test converted from assert result['success'] to pytest.skip() on timeout | The suite reported green. The test can now never fail; it either passes or silently skips. |
| A production security hardening task declared ready without the review step being run, with eight security issues found afterward — two of them critical | Completion was asserted rather than evidenced, and the assertion was accepted. |
The pattern in both: a criterion that the agent was allowed to self-certify. Checkable criteria constrain this in a way interpretable ones cannot. curl returns 429 has a failing state. “Well-structured code” does not.
Two Defences That Cost Almost Nothing
The first defence is an explicit prohibition in the task list itself. Add a line to every task block that involves tests:
You may not modify existing test assertions or add skip markers to make tests pass.
If a test is failing, fix the source code.
This does not make the model incapable of editing tests. It makes test-editing a policy violation rather than an optimisation target, and Claude Code’s instruction-following is strong enough that explicit prohibitions carry real weight.
The second defence is a verification step written into the spec, not left to review. After the execute phase, before you read the diff, run:
git diff --name-only | grep test
grep -r "pytest.skip\|unittest.skip\|xit(\|xtest(" tests/
If either command returns output that was not there before the session, stop and read those files first. The diff can wait.
Review with a Context That Never Saw the Plan Being Written
The same logic that argues for a fresh execution session argues for a fresh review session. By the time you finish a build, you have read so many partial states of the code that you will miss things a cold reader would catch immediately.
Open a new session, attach the spec, and use a prompt along these lines:
Read SPEC.md and then read the diff below. For each acceptance criterion in the spec,
tell me whether the diff satisfies it, partially satisfies it, or does not address it.
Do not look at intent. Look at what the code actually does.
<diff>
[paste diff here]
</diff>
The key instruction is the last one. Intent is what the implementation session was reasoning about. A review that also reasons about intent will miss the same gaps.
Do You Still Need a Framework?
Several frameworks have grown up around this pattern. The question worth asking is what they add that a well-written markdown file does not.
The honest answer is: mostly scaffolding and a forcing function. If you will reliably open a new session, write checkable criteria, and run a verification step, you do not need the framework. If the process needs to be enforced by tooling because it otherwise gets skipped under deadline pressure, the framework is doing real work.
The failure modes are the same either way. A framework that generates interpretable criteria is not safer than a markdown file with interpretable criteria. The criteria are the load-bearing part.
Scaling to Parallel Work
Once the task list is written with explicit dependencies, parallel execution becomes straightforward. Tasks with no shared dependencies can run in separate Claude Code sessions simultaneously, each reading the same spec but working on isolated parts of the codebase.
The practical limit is merge conflicts and shared state. Two sessions editing the same file will produce a conflict. Two sessions that each own a separate module and communicate only through an agreed API contract will not. Designing for parallelism at the spec stage — specifically, at the point where you define which files each task touches — is cheaper than resolving conflicts at the merge stage.
A useful addition to the task table is a “touches” column listing the files or directories each task is allowed to modify. This makes dependency and conflict analysis visual rather than implicit.
What Changed in How I Work
Three things shifted once this process was running consistently.
The first is that planning conversations got longer and build sessions got shorter. A planning session that surfaces twelve open questions before a line of code is written is not slower — it is cheaper than discovering those questions in the middle of a build.
The second is that review became a comparison rather than an inspection. When the spec lists twenty checkable criteria and the diff addresses nineteen of them, you know exactly what to ask about. When the spec says “handle errors properly”, review is guesswork.
The third is that the spec file became the persistent artifact rather than the code. Code changes. The spec records why it changed, what constraints were active, and what was explicitly left out. That context is what makes the next feature cheaper to specify.
The pattern is not complicated. Write criteria a command can settle, execute in a fresh session, and verify against the spec rather than against your memory of the plan. The hard part is not learning the process — it is resisting the pressure to skip steps when a deadline is close, which is exactly when the steps matter most.
Frequently Asked Questions
What is spec-driven development with Claude Code? Spec-driven development is a workflow where you write a complete requirements and design document before Claude Code writes any code. The spec defines acceptance criteria, data models, API contracts, and an ordered task list. Claude executes against that document in a separate session, reducing the number of implementation decisions it makes autonomously.
Why does executing in a fresh session matter? By the end of a planning session, the context window holds every rejected idea, clarifying question, and file Claude explored. Building in that same window means implementation decisions are reasoned against discarded alternatives. A fresh session treats the spec as a clean interface with no memory of how it was produced.
What is EARS notation? EARS stands for Easy Approach to Requirements Syntax. It is a set of five sentence templates — WHEN, IF/THEN, WHILE, WHERE, and unconditional SHALL — that force authors to name the trigger, condition, and required system response explicitly. Criteria written in EARS map closely to test cases and leave little room for interpretive shortcuts.
How do I stop Claude from editing tests instead of fixing bugs? Add an explicit prohibition to every task block that involves tests: state that existing test assertions may not be modified and skip markers may not be added. Also run a post-session grep for skip markers and check the diff for test file changes before reviewing anything else. Checkable acceptance criteria reduce the incentive for this behaviour in the first place, since there is a clear external signal the agent cannot fake.
Do I need a framework or is a markdown file enough? A well-structured markdown file with checkable criteria is sufficient if you will reliably follow the process. Frameworks add scaffolding and enforcement, which has value under deadline pressure. The criteria themselves are the load-bearing element — a framework that generates interpretable criteria is no safer than a markdown file with the same problem.