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 the 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. The user then found eight security issues, 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
First, ask for evidence rather than assurance. Tell Claude to show the command it ran and the output it got, not a summary of how it went. Reading evidence takes seconds and is the only way to review a run you were not watching.
Second, put the anti-gaming rules into the criteria themselves. If the spec says the diff must not add skip markers or delete assertions, then doing so is a spec violation rather than a clever shortcut:
## Verification rules
- pytest exits 0 with zero skipped tests
- git diff adds no @pytest.mark.skip, pytest.skip, or .only
- git diff deletes no existing assert lines
- Every criterion in section 2 has at least one test naming it
Phase 3: Three Ways Specs Drift Mid-Build
Even a good spec loses its grip as a build gets longer. An issue thread from April 2026 categorised the ways this happens, and the categories match what I have seen:
| Drift mode | What happens |
| Ignored | The rule is in context and readable, and gets passed over during execution anyway. |
| Forgotten | Context fills with code as the build progresses, and the rule falls out of effective attention. |
| Skipped | The rule is read and judged unnecessary for this particular case. |
The blunt summary from that thread, paraphrasing the agent’s own behaviour: it can recite the rules, it just does not follow them. Which tells you something important. A rule living in a document is advisory. Restating it more forcefully does not change its category.
What actually held: of the approaches tried in that thread, only pre-commit hooks proved reliable. A hook is code. It runs whether or not the rule was in attention.
A gate script does not need to be clever. Grep the diff for skip markers, check that each criterion identifier in SPEC.md appears in at least one test file, exit non-zero with a readable message. Claude reads the failure and fixes it, which is exactly the loop you want.
Phase 4: Executing the Plan Without Losing the Thread
Once the spec and task list exist, the execution session is where the discipline pays off or does not.
Give the session a goal condition, not just a prompt
A goal condition is re-checked by an evaluator after every turn, so the session ends when the condition holds rather than when the output looks finished:
> /goal All 5 acceptance criteria in SPEC.md have a passing test,
and git diff --stat shows no changes outside src/auth/ and
tests/auth/

Notice the second clause. Scope creep is the most common way a build technically satisfies a spec while doing things you did not ask for, and it is easy to state as a checkable condition.
One task, one subagent, one commit
For anything past a handful of tasks, give each task its own subagent. Each one starts with a clean context, reads only PLAN.md and the files its task needs, and commits atomically. The exploration and file reading stay out of your main window, and a bad task is one revert rather than an untangling job:
> Work through PLAN.md in order. Give each task its own subagent.
One commit per task, and stop if any task fails rather than
working around it.
The instruction to stop rather than work around a failure is doing real work in that prompt. Task 5 failing because the spec never mentioned Redis is useful information. Task 5 silently being implemented with an in-memory counter is not.
Review with a context that never saw the plan being written
Before calling it done, have a fresh subagent check the diff against the spec. Because it never saw your reasoning, it judges the result on its own terms:
Use a subagent to review the full diff against SPEC.md.
For each acceptance criterion, state whether it is implemented
and which test covers it. Flag any file changed that the spec
did not put in scope. Report gaps, not style preferences.
Do You Still Need a Framework?
Most spec-driven development writing from last year assumes you install something. That assumption is worth revisiting, because Claude Code has since shipped native primitives for most of what those frameworks provided.
| What you need | Native Claude Code | Framework |
| Interview to produce a spec | AskUserQuestion in plan mode | Spec Kit /specify, /clarify |
| Read-only planning phase | Plan mode, Ctrl+G to edit | Kiro’s spec phase |
| Ordered task list | Ask for it in the plan | Spec Kit /tasks |
| Condition that gates completion | /goal with an evaluator | Not typically provided |
| Hard gate on completion | Stop hook or pre-commit hook | Not typically provided |
| Task isolation | One subagent per task | BMAD role agents |
| Reusable workflow | A skill with a slash command | Framework slash commands |
Scaling to Parallel Work
One subagent per task is sequential by default. If your task list has independent branches, two options open up.
Agent teams
Agent teams shipped with Opus 4.6 in February 2026 and are still experimental. A lead session spawns teammates, each with its own context window, and they coordinate through a shared task list and a mailbox rather than only reporting back to a parent. You enable it with an environment variable:
# in settings.json or your shell
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
The cost is real. Teams use roughly seven times the tokens of a single session in plan mode, per Claude Code’s own cost documentation. For a multi-module feature where the modules really do not touch, that can still be worth it. For a linear task list it is waste.
Know the ceiling
Reported experience puts the practical sweet spot at three to five concurrent agents. Past that, coordination overhead and merge conflicts eat the parallelism. This matches what I have seen: the second and third agent feel like a clear gain, the sixth feels like managing a standup.
For much larger fan-out, Dynamic Workflows can coordinate far more subagents in a single session, which we covered in our Claude Opus 4.8 article. And for how these pieces assemble into repeatable systems rather than one-off runs, Loop Engineering for AI Agents is the wider view.
Honest Limits
Things spec-driven development in Claude Code does not give you, which are worth knowing before you bet a production build on it:
| Limit | What it means for you |
| No native drift detection | Nothing automatically notices when the build stops matching the spec. A hook is your detection layer. |
| No guaranteed spec compliance | The spec is context, not a constraint. Only deterministic gates are enforcement. |
| Multi-agent coordination is not reliable yet | Agent teams remain experimental. Treat parallel work as something to supervise. |
| Specs go stale | A spec written three features ago may describe behaviour that has since changed. Date them and mark what they supersede. |
None of these are reasons to skip the spec. They are reasons to put the enforcement in code rather than in prose.
What Changed in How I Work
Spec-driven development gets sold as a productivity technique. In practice the gain is not speed on any one feature. It is that the failure mode moves to a place where you can catch it.
Without a spec, a wrong decision surfaces when you read the diff, or later, when something breaks. With one, it surfaces while you are answering an interview question about a case you had not considered. That is a much cheaper place to be wrong.
The part that took me longest to accept is that the spec is not the artifact doing the work. A document full of intentions is advisory, and an agent optimising for a signal will find the shortest path to that signal. What does the work is the small set of things that can return a non-zero exit code.
If you take one thing: write every criterion so a command can settle it, then put the ones that matter behind a hook. That single change does more than any amount of additional spec prose.
The primitives referenced here come from the official Claude Code best practices documentation, and GitHub Spec Kit is the reference framework implementation if you want to compare. For the session flags and commands used throughout, see the CLI commands worth knowing.
Frequently Asked Questions
A. Long enough to remove the decisions you care about, and no longer. A spec for a feature of a few days’ work is usually one to two pages. If it is longer than the code it produces, you are writing documentation rather than a spec. The test is whether each line removes a decision Claude would otherwise make.
A. They are related but the primary artifact differs. TDD treats a failing test as the thing you write first. SDD treats the specification as primary, with the tests generated from its acceptance criteria. In practice they compose well: write criteria in a shape that maps to test cases, and the first execution task becomes writing those tests.
A. By the end of planning your context is full of rejected options and files read during exploration. Implementing in that same window means every decision is reasoned against discarded alternatives, and you burn context you will need for the build. A fresh session reads the spec as a document, which is what it was written to be.
Login to continue reading and enjoy expert-curated content.

