How I work with Claude Code: PRD to deploy
I write the PRD, we agree an execution plan, pick the environment, get the keys out of the way, prototype, then test. Six steps, same order every time. Here's the real workflow with the files from two things I've shipped, and which model I hand each job to.
I write a PRD, hand it over, and go and make a coffee. By the time I'm back there's a branch, a passing test suite, and a diff worth reading - and the reason that works, the boring truth of it, has almost nothing to do with clever prompting.
It's the scaffolding. A spec I wrote myself before anyone touched a file, a plan we agreed out loud, the keys parked somewhere they can't leak, and a way of checking the work that doesn't rely on the agent's own opinion of how it went (which is, reliably, sunny). Six steps, same order every time. What follows is the workflow itself, with the real files from two builds I've shipped this way - YubHub, a jobs platform, and an SEO audit server that runs inside Claude - plus the part nobody writes down, which is which model I hand each job to and why I don't just throw the expensive one at everything.
Quick Navigation
1. I write the PRD |
2. The execution plan |
3. The environment |
4. Secretise the keys |
5. Prototype |
Which model, which job |
6. Test it properly |
What's immutably true
Step one: I write the PRD
This is the bit everyone wants to hand to the model, and it's the one bit you can't. I write the PRD myself - by hand, usually in a scratch file before I've even opened the repo - because a PRD (product requirements document, if you've not met the term; it's a hangover from the days when a product manager would hand engineering a stack of them and disappear) is where you do the thinking you'd otherwise end up doing badly, at speed, halfway through a build with a model idling on you and the clock running. I've learned this the expensive way more than once: if I can't sit down and write what "done" looks like, the model can't hit it either, and the whole session quietly turns into a negotiation with my own vagueness, which is a fight nobody wins.
Mine tend to run short, though I'd never optimise for that - length isn't the test. The only test I trust is whether someone who wasn't inside my head that morning could pick the thing up, build it, and know for themselves when they'd finished.
Here's the shape, and you're welcome to lift it wholesale:
# PRD: [what this is]
## The outcome
One sentence. What exists at the end that doesn't exist now.
## Why
The problem this solves. If I can't write this, I stop.
## Scope
IN: [what this build includes]
OUT: [what it explicitly does not - this is the useful list]
## Definition of done
- [ ] A testable statement, not a feeling
- [ ] How I'll know it works: [the command, the URL, the check]
## Constraints
Environment, versions, anything already load-bearing that must not move.
## Open questions
What I don't know yet. Left visible, not guessed. The two sections that earn their place - and they're the two people delete first, which tells you something - are OUT and open questions. Scope creep is the default failure of an agentic build (the model is agreeable to a fault, and left alone it will cheerfully keep going, adding the thing you didn't ask for), and an explicit OUT list is the cheapest brake there is. Open questions matter for a subtler reason: writing "I don't know yet" is how you stop a confident wrong answer getting quietly baked in at step five, where it's ten times harder to dig back out.
When I planned seo-audit-console - an MCP server that pulls Search Console history and a first-party crawl into one prioritised audit, right inside a Claude conversation - the planning folder ended up split along exactly those lines without my deciding it should. The architecture and tool surface as the what. A file literally called open-questions.md for the decisions I hadn't made yet and didn't want to fake. A roadmap and a progress checklist as the how. That wasn't a method I'd read in a blog post; it's just the shape the work kept asking for, and I've used it on everything since.
Step two: we write the execution plan
The PRD says what. The execution plan says how, in what order - and it's the first thing we do together, out loud, rather than something I hand down and hope lands.
This is the step where I stopped trusting my own diligence, and I'd recommend the same distrust to anyone. The pattern that kept biting me: a long session, a plan agreed cleanly at the start, and somewhere in the sprawling middle a couple of steps had quietly not happened - no malice in it, nobody skiving, just nobody checking. So the plan is now a file, and the trick that makes it work is that it gets filled in as the work happens, not written up as a tidy summary at the end:
| Phase | Done | Evidence |
|---|---|---|
| Research | YES | files written, sources verified |
| Build | YES | the command that ran, its real output |
| Gates | YES | each check, PASS/FAIL recorded |
| Deploy | PENDING | |
**Deviations (declared when they happen, with why):**
- [none / list] The evidence column is the whole game. "Done" on its own is an assertion, and assertions are exactly what a long agent session is full of. "Done, and here's the output that proves it" is a fact you can stand on. A row filled in retrospectively, at the end, once everyone's already decided it went well - that's worth precisely nothing, and I've caught myself doing it, which is how I know.
Deviations are allowed - I want to be clear about that, because a plan that forbids deviation just gets lied to. Plans meet reality and reality wins. The single rule is that a deviation gets declared when it happens, with its reason, rather than confessed sheepishly afterwards.
The best version of this I've written started with a code review instead of a feature list, and I'm glad it did. The finding that gated the entire audit-server build: the existing crawler ran fully synchronously, so any large crawl would blow straight through the tool-call ceiling, lose its result and orphan itself somewhere I couldn't see. I wrote it down at the time, more or less verbatim - this is why crawl performance is "unreliable"; it isn't slowness, it's the wrong execution model. Everything else in the plan queued up behind that one fix. Knowing it before I'd built anything saved me from decorating a broken foundation and calling it a house.
Step three: I choose the environment
Before anything gets built I settle where it runs and what the rules are in that particular place - which sounds like admin, and mostly is, right up until the afternoon it isn't.
Some of the choices are obvious: which repo, which branch, local or deployed, whether a long agent run needs its own worktree so it can't tread on live work while my back is turned. The part people skip, and it's the part that pays off, is telling the agent what it's standing in.
Every repo here, and most of the sub-folders, carries a CLAUDE.md. It isn't documentation for humans - nobody reads it over coffee. It's the environment's rulebook, loaded automatically at the start of every session, holding the things that are true about this place and expensive to rediscover: where the credentials live, which script is the canonical one, the exact deploy sequence, the traps that have already caught me. One of mine carries this, near the top, in slightly aggrieved capitals:
## Deploy hygiene
NEVER build while the dev server is running - it corrupts the bundle and
routes go missing at random. Kill dev processes first, then clean build.
Health-check every route after the deploy, before you call it done. That rule cost me the better part of an afternoon, blaming everything except the real culprit, before it earned its place in the file. And that's the real function of the thing - it's where a lesson goes to be learned once, so the next session inherits the scar tissue instead of growing its own.
YubHub - my jobs platform, which discovers and enriches postings from company career pages and publishes structured feeds I can query - took this further than I meant it to. Its planning lives in nine numbered documents, architecture through to deployment, written deliberately purpose-agnostic so the scaffold could be reused for the next build rather than rewritten from scratch. That wasn't the plan when I started; it became obviously the right call about three documents in, which is roughly when most good conventions announce themselves.
Step four: secretise the keys
Short section, because the rule underneath it is absolute and doesn't reward elaboration: keys never live in a tracked file. Ever.
The pattern is dull and load-bearing. A .secrets/ folder, gitignored on the first commit before it holds anything, keeping the API keys and application passwords the workflows need; everything else reads from it at runtime. The path I prefer goes one better and never writes the key to disk at all - it pulls the secret out of a password manager and injects it as an environment variable for the length of a single command, then it's gone. The gitignored file is the fallback for when that's more ceremony than the moment deserves. Neither route ends up in a commit, a prompt, or - and this one catches people - a screenshot.
Do this at step four rather than step six, and do it before you think you need it, because the moment you're deep in a prototype you will want a key in a hurry, and "I'll just paste it in for now" is the exact sentence that ends with a live credential sitting in your git history forever.
Two habits ride alongside it. Rotate anything that's been pasted somewhere it shouldn't have been - assume it's burnt the second it's exposed, because assuming otherwise is how you find out the hard way. And when you screenshot your own terminal for an article like this one, look at what's in the frame before you hit publish.
Step five: prototype
Now, finally, build the thing.
The prototype's only job is to be run, not admired - and I have to remind myself of that, because there's a strong pull toward making it tidy first. What I want is the smallest version that executes against real inputs, because real inputs are where the PRD's assumptions meet a world that has never read the PRD and doesn't care what it promised.
The single habit that moved my hit-rate more than any prompting trick: I want the real output, immediately, in front of me. The response, the error, the file it wrote to disk - not a summary of what happened, not the model's account of its own success. The gap between "I've implemented that" and what is on disk is exactly where the day disappears, if you let a confident summary stand in for a look.
Which model gets which job
This is the part I get asked about most and see written down least, which is a shame, because it's most of the economics.
I don't run everything through the most capable model available. That's expensive, and more to the point it's unnecessary - most of the work in any real build is bounded and well-described, and bounded-and-well-described is precisely the work a cheaper model does perfectly well while the good one sits there costing money to watch.
Inside Claude Code
Switching is a runtime decision, not a config file you set once and forget. /model, mid-session, whenever the shape of the work changes under me. The rule of thumb states itself easily enough: the expensive model does the reasoning, the workhorse does the volume. An architecture call, a gnarly cross-file refactor, anything where being wrong is costly and quiet - that's worth the premium. Implementing an already-agreed plan against an already-clear spec is not, and I'll drop down a tier for it without any ceremony at all.
Out to a local model
Bounded, self-contained pieces go off the Claude bill entirely, through houtini-lm - an MCP server I built for exactly this, because I got tired of paying frontier prices for boilerplate. A first-pass code review, format conversion, test stubs, a commit message, the bulk churn. Claude stays the orchestrator and does the thinking; the local model executes the heavy, dull pieces underneath it. On my machine that's a pair of modded 48GB RTX 4090s serving through vLLM, though the useful idea here is the split, not the hardware - you can run the same pattern against a rented endpoint and lose nothing but the electricity bill.
Out to someone else's endpoint
houtini-lm points at anything OpenAI-compatible, which is the quiet feature that makes it worth more than the hardware behind it. In my setup it points at a small router I run locally (http://127.0.0.1:4000), and the router in turn fans out to whatever's cheapest for the job - the local rig for free volume, DeepSeek's API for the cheap-but-capable middle - each picked by name on the call. Swapping in a hosted model is a line of config and its key.
And here's the part that's changed since I first sketched this workflow, because the field does not sit still: I stopped taking the vendor numbers on faith and ran my own bounded gauntlet across every tier - the local rig, DeepSeek Flash, DeepSeek Pro - on one small, verifiable coding task. All of them wrote correct code; what separated them was cost and wait, not whether the answer was right, which is the entire argument for routing rather than crowning a favourite. The measured results are their own article - the short version is that the free local tier came back fastest, because the hosted models are reasoners that think for ten to twenty-odd seconds before they type a character. One honest caveat I'll keep in: I tried wiring Moonshot's Kimi into the same router and pulled it back out, because it hung on the larger requests - a reminder that "OpenAI-compatible" is a family of dialects, not a standard, and the interop bites where you least expect it.
Step six: test it properly
Then the part that decides whether any of the preceding five were real, or just felt real.
Checks that fail loudly
Anything a script can check, a script checks - and I mean the boring, unglamorous validators, because those are the ones that run. Here that's a little suite of them: one greps a draft for the specific patterns our publishing pipeline is known to mangle and exits non-zero the moment it finds one, others count things that are supposed to be zero and shout if they aren't, and a regression test pins a contract we've broken before so the next refactor breaks it noisily instead of silently at 2am. Every single one of those exists because something got through once and cost me for it. That's the origin story of most test suites, if people were honest about it.
YubHub carries somewhere around five hundred test cases across roughly thirty files, and still gets a live smoke run after any infrastructure change - because "the tests pass" and "the deployment works" are two different claims, and conflating them is a classic way to ship a green build to a dead server.
A finding needs evidence, not an adjective
When something's wrong I want it written down in a fixed shape, not described. The audit server's findings folder spells the shape out and I've stuck to it since: each finding gets a symptom, a root cause, evidence (file:line, or the test output itself), a severity, and a fix sketch.
That format does something quietly powerful. "The crawler is slow" is an adjective, and adjectives are where investigations go to stall. "The crawler blocks the tool call at run-seo-audit.ts:80, so anything over sixty seconds orphans itself" is a fact with a street address, and you can act on it before you've finished reading the sentence.
A fresh pair of eyes
Review happens in a fresh context, deliberately, not tacked onto the end of the session that built the thing. A session that's been grinding for two hours has every psychological reason to believe it succeeded - it's been telling itself so the whole way. Starting clean, with nothing but the diff and the definition of done from the PRD, catches the class of mistake the builder is structurally incapable of seeing, in the same way you can't proofread your own writing an hour after you've written it.
Then I look at it myself
The last check is human and it isn't optional, ever. "The tests pass" and "this is the thing I actually asked for" are different claims, and only one of the two can be automated. So I read what ships. All of it.
What is immutably true
The tools underneath all of this have changed repeatedly, and fast - the models, their versions, the entire local serving stack got ripped out and replaced this month alone. These few things, though, haven't moved an inch in two years of doing it.
Your feedback loops are the ceiling
If the work can't be checked quickly and unambiguously, no amount of prompting rescues it - I've watched clever prompts die on this hill repeatedly. A codebase with fast tests and clear errors gets good output almost regardless of how you talk to it. One where "did it work?" means twenty minutes of manual poking gets confident nonsense, because the model never gets a straight answer to correct against. Everything else in this article is downstream of that one fact.
Verify, don't trust
Not because the model lies - it isn't lying - but because a self-report simply isn't evidence, however sincerely it's offered. It's why the plan has an evidence column and why review happens in a clean room. Trust that survives a long session unexamined isn't trust; it's just inattention wearing trust's coat.
Write it down or learn it twice
Anything discovered the hard way goes straight into a file that loads automatically - the CLAUDE.md, the procedure, the log - because session memory ends the instant the session does, and a lesson that lives only in a closed conversation is a lesson you're going to pay for again. A repo that has learned something keeps it, and hands it to the next session for free.
Delegate the bulk, keep the judgement
Bounded, well-described work goes to the cheapest thing that can do it properly, and it does it properly more often than pride wants to admit. Cross-file reasoning, architecture, the calls where being wrong is expensive - those stay with the good model, and with me.
Scope is a brake
The OUT list is the most useful line in the whole workflow, and it's a single line. An agent will follow you anywhere, enthusiastically, right off a cliff if the cliff wasn't explicitly ruled out.
Secrets never touch a tracked file
No exceptions, no "just for now", no "it's only a test key". The graveyard is full of test keys.
Where the review boundary sits, mind you, I'm still not settled on - and I've been at this a while. Some of what I check by hand could probably be scripted, and some of what I've already scripted I end up reading anyway because I don't quite believe it. That balance keeps shifting under me and I haven't found the right place to plant it yet. I'll let you know if I do.
Where to start
If you're setting this up from scratch, do it in this order, and please don't skip ahead to the fun part - the fun part is where all the damage happens.
Write one PRD by hand, for something small you want built, and write the OUT list first. Agree the plan before any code, with the evidence column baked in from the start rather than bolted on. Get your keys into a gitignored folder before you need them, not after. Prototype small, run it early, and look at the real output rather than the summary of it. Then check the thing three ways: the script, the fresh context, and your own eyes.
The scaffolding is the boring part, and - I keep coming back to this - the boring part is the part that works. If you're new to Claude Code, you can start with a free week and give it one real job from your backlog. One you've written a PRD for.