← Back to Blog

Spec-Driven Development with Claude Code: 3 Ways the Spec Itself Broke Us

Three months ago I finally lost the argument. I wrote a spec before letting Claude Code touch the code, and it worked. Fifteen minutes of OpenAPI saved five PR rounds of “why is my checkout applying coupons to itself.” I felt like an adult.

Then I moved to spec-kit, ran a real feature through it, and watched the spec become the bug.

The code was fine. The tests passed. The commit was clean. What broke was that the spec I had written six weeks earlier no longer described what the system did, and Claude Code kept generating things faithful to the spec instead of faithful to reality. This is a different failure mode. I don’t get to laugh at it the way I laughed at the coupon-eating discount function, because this one was slower, quieter, and I was the person who wrote both the spec and the drift.

This post is about the three ways the spec itself broke us, and the four guardrails I keep now. If you already run SDD with Claude Code or Codex-style official agents, you have probably hit at least one of these.

Failure 1: The spec drifted and I could not see it

The first run was a payment webhook. I wrote a clean spec: endpoint, request shape, retry policy, idempotency key location. Claude Code shipped the handler in one pass. Six weeks later a teammate moved the idempotency key from the header to the request body, because a partner’s SDK could not sign headers reliably. The change was three lines in the handler and a note in the PR description. Nobody updated the spec.

Three sprints after that I asked Claude Code to add a refund flow to the same webhook. It read the spec, saw idempotency-in-header, and generated a refund handler that pulled the key from the header. The unit tests passed because the test fixtures still used headers. Integration test caught it, but only because we happened to have one. Without that test, we would have shipped a refund handler that silently double-refunded any partner using the new SDK path.

The failure mode is: the spec is now a lie, and the agent trusts it more than the code. Agents do not read code to disambiguate. They read the artifact you told them was authoritative. If your spec is stale, your agent is confidently wrong. Community tools like the spec-kit-sync extension have started to detect this by comparing specs against implementation and flagging drift, aligned requirements, and unspecced code features. That existing at all tells you how common the pattern is.

The lesson I keep: a spec behaves like a snapshot, not a contract. Snapshots go stale. Contracts hold up because a compiler yells at you when you break them, and nothing yells at you when you break a spec.

Failure 2: The ambiguity that only mattered at scale

Second run was a bulk-import CSV endpoint. Spec said: “on parse error, return 400 with the error message.” Claude Code generated exactly that, and I was pleased with myself for writing such a clean bullet point.

The first user uploaded a 40,000-row CSV. Row 14,000 had a bad date format. The handler returned 400. The user re-uploaded the fixed file. Row 22,000 had a bad enum. 400. Re-upload. Row 31,000, a stray comma. 400. It took the customer six uploads to get a clean run, and they were furious, because our competitor’s importer returned every bad row at once so you could fix them in one pass.

The spec was not wrong. It was underspecified in a way that only shows up under load. “Return 400 with the error message” quietly assumed “one error, one row, one message.” Claude Code took the shortest path from prose to code. It had no reason not to. There was nothing in the spec that said “collect all errors and return the list,” because when I wrote the spec I had never watched a 40k-row upload fail on row 14,000 six times in a row.

I now assume every spec bullet has a hidden singular-to-plural failure mode. If it says “on error,” I ask myself “what if there are three thousand?” If it says “the user,” I ask “what if there are ten?” Half the time the answer is “unchanged, ship it.” The other half the answer would have cost a customer, and I am glad I asked before Claude Code wrote code that was faithful to my thin prose.

Yes, this is the same lesson every senior engineer has had beaten into them by an outage. SDD does not exempt you from it. It just changes what artifact the outage came from.

Failure 3: The coupling that lived only in the spec

Third failure was the sneakiest. Spec described three endpoints: create user, create workspace, create billing account. Each had its own file. Each was independently reviewed. Each looked clean.

The problem was that the spec quietly encoded a coupling: create-user assumed a workspace existed for the returned default_workspace_id, and create-billing-account assumed a user with a stripe_customer_id. That coupling was not written down anywhere. It only existed in my head from having authored all three specs in the same afternoon.

Claude Code generated three handlers independently, one per spec file. Each was correct in isolation. The integration test that exercised sign-up-to-first-payment failed because create-user’s default_workspace_id came back as null, because nothing in create-user’s spec said “call workspace creation first.” I had known that. I had not written it down. The spec looked complete because each file looked complete.

This is the coupling nobody warned me about: spec files, like microservices, present an illusion of independence that the runtime does not honor. Agents will happily generate three correct-in-isolation handlers that break the moment they touch each other. If your spec is one document per endpoint, you own the cross-endpoint invariants somewhere. If you own them in your head, the agent does not have them.

I now write a “cross-endpoint invariants” section at the top of every spec set, listing the assumptions no single file can enforce. It reads like the boring part of a design doc. It is also the part that stops Claude Code from generating three lovely handlers that hate each other.

The 4 guardrails I keep now

After three runs of the spec being the bug, I stopped treating specs as gospel and started treating them the way I treat any other artifact that can rot.

The 4 guardrails for spec-driven development

Guardrail 1: Version-lock the spec to the commit that shipped it

Every merged PR now includes a spec hash in the commit message. If the code changes and the spec does not, git blame will show me the spec version the code was generated from. When Claude Code reads the spec next time, I know whether the spec-code gap is a day old or six months old. This is the single change that catches Failure 1 fastest.

I stole this from the way we already lock schema migrations to commits. Specs are just prose migrations. Treat them the same.

Guardrail 2: Compile the spec to at least one acceptance test

Every spec file gets one acceptance test that fails if the code diverges. Not the exhaustive test suite. Just one test that exercises the happy path described in the spec, in the shape the spec claims. When the spec drifts, that test starts failing. When someone updates the spec, they update the test in the same PR, or CI blocks the merge.

This differs from TDD. TDD would have you write tests before code. Here the spec becomes executable in one small way, and that one way is enough to notice when reality moved.

Guardrail 3: Mark agent-only fields explicitly

Some fields in a spec exist for the agent to hook onto, not for the runtime. Examples: “must return within 200ms,” “must not log the request body,” “must be safe to retry.” Runtimes do not enforce these. Agents can. I now mark these with an agent: prefix in the spec, so both the agent and the reviewer know these are behavioral constraints, not shape constraints. When the spec drifts, the agent-only fields are the first to lose meaning, and marking them makes that easier to see.

Guardrail 4: Quarterly spec-freshness review

Every quarter I run through the spec directory and flag anything that has not been touched in 90 days but whose corresponding code has. That list is usually short. It is also usually where the next bug is going to come from. Spec-kit itself is starting to acknowledge this — the community has a spec-adherence scoring extension that runs over the spec/code delta and gives you a number. I use a home-grown version that just lists the files. Either works. What matters is that the list gets looked at.

Ninety days is my number, not gospel. Pick a cadence that matches how fast your product moves. If you ship every day, use 30. If you ship every quarter, 180 is fine. What is not fine is “whenever someone remembers.”

What I actually think about SDD now

I still write specs before letting Claude Code touch anything of consequence. The alternative is asking an agent to guess my intent from prose, and that path leads to discount functions that give coupons a coupon. That was the argument I lost, and I have no interest in losing it again.

But I no longer believe the spec is the finish line. The spec is the second thing that can rot after the code. The reason it feels safe is that specs are prose, and prose looks fine even when it has quietly become a lie. Agents cannot tell. Reviewers can, but only if the reviewer is you, and you wrote the spec six months ago, and you have not touched the code in three sprints.

The four guardrails above are unglamorous. They are what I had to build to stop pretending the spec was the trustworthy artifact just because I had written it in YAML. If you are running SDD in 2026 and you have not hit at least one of these three failures yet, the honest read is that you are still early.

For the full argument on how I actually run Claude Code end-to-end — including which parts of the workflow I refuse to hand to any agent — see Claude Code Mastery.

Practical Claude Code Related book Practical Claude Code The field guide for engineers who use Claude Code every day — CLAUDE.md, Plan Mode, and team workflows from a year of real production use View the book page →