You typed one line: "add rate limiting to the API." The agent wrote four hundred lines — a middleware, a Redis dependency you didn't approve, a config module, and a test file that asserts almost nothing. Forty minutes later you're untangling work you never asked for, in a shape you wouldn't have chosen, and you're still not sure whether the thing actually rate-limits anything.
The instinct is to blame the model. It's the wrong instinct. The model did exactly what you asked. The failure happened before you pressed enter.
I've written before about the harness — the rules, skills, and memory that turn a people-pleasing agent into one that argues back — and about the files that make it real. That was about how the agent behaves. This series is about how you work: planning, context, delegation, guardrails, and the one that matters most, reviewing before you push.
Five parts. Claude Code in the examples, portable in the principles; the working code lives in agent-workflow.
An underspecified prompt doesn't produce nothing — it produces guesses
This is the part people miss. If you hand a junior engineer a one-line ticket with five open questions, they come back and ask. An agent doesn't. It resolves every ambiguity itself, silently, instantly, and with total confidence — and then builds on top of those resolutions.
Go back to "add rate limiting to the API." Look at what you didn't say:
- Rate limit per what — IP, API key, user, tenant?
- What limits, and are they the same for every endpoint?
- In-process or shared across replicas? (This one decides whether you get a new infrastructure dependency.)
- What happens on breach — 429, queue, drop?
- Does this apply to internal service-to-service calls too?
Five decisions. You made none of them; the agent made all five. It picked Redis because a shared counter is the technically correct answer to a question nobody asked, and now a dependency has entered your project through a one-line prompt.
The output wasn't wrong. It was an answer to a different question than the one you meant.
Make the agent interview you
The highest-leverage habit in this whole series takes one sentence: stop asking for code, and start asking for questions.
Before writing any code: ask me what you need to know.
Constraints, intent, and risk. Don't propose a solution yet.
That's it. In Claude Code you get this structurally with plan mode — it explores and plans without touching files. In Cursor, Copilot, or Codex you get it by simply instructing it, which works better than most people expect. The principle is tool-independent: separate the "what and why" turn from the "write it" turn.
Two things I've learned to watch for once you start doing this.
Good questions are about constraint, intent and risk — not preference. "Should I use Redis or in-memory?" is a preference question and it's the agent stalling. "Do you run more than one replica, and must the limit be shared across them?" is a constraint question — it decides the design, and only you can answer it.
If the agent has no questions, that's information. Either the task genuinely is trivial, or it hasn't understood it and is about to guess confidently. On anything non-trivial, silence is the second one.
A plan in a chat window is not a plan
Once the questions are answered, make the agent write the plan to a file — not into the conversation.
This sounds like bureaucracy. It's the opposite: it's the cheapest leverage available to you, because a plan in a file is reviewable, diffable, and survives the session. A plan in a transcript dies when the context window rolls over, and it can't be handed to anyone.
A plan worth writing has six parts:
- Goal — one sentence, in terms of behaviour, not implementation
- Constraints — the answers from the interview, written down
- Files to touch — named up front, so you can spot a blast radius before it happens
- Approach — the actual steps, in order
- Out of scope — explicitly listed
- Verification — the command or test that proves it worked
The "out of scope" section is the one everyone skips, and it's the one that saves you. Agents drift — they "improve" the auth middleware while adding rate limiting, they reformat a file they only needed to read. Writing "do not modify the auth layer; do not add new dependencies; do not reformat unrelated files" into the plan turns drift from something you discover in review into something you agreed about in advance. We'll come back to scope drift in part five, because it's one of the signatures of agent-written code that looks right and isn't.
Size every task to the diff you'll actually read
Here's a rule I hold hard: the size of a task should be the size of a diff you are genuinely willing to read, line by line, today.
If a plan step produces a 900-line diff, be honest — you will not review it. You'll skim it, see plausible-looking code, and approve it. And once that happens, every control downstream is theatre: the tests you didn't check, the CI that passes because the assertions are weak, the approval that means nothing.
So decompose until each step is one commit, one purpose, one readable diff. This is slower for exactly one iteration and faster for every one after, because the moment something goes wrong you're bisecting three files instead of thirty.
Say what "done" means before you start
The last line of any plan is the verification step, and it has to be concrete enough to run:
Done means:
pytest tests/test_rate_limit.pypasses, a sixth request within one minute returns 429, anddocker compose upstill boots cleanly.
If you don't define this, the agent defines it for you — and its default bar is "the code exists and imports." I've watched a lot of engineers discover, an hour in, that they and the agent had completely different ideas about what finished looked like.
What this actually looks like
Here's the whole thing in one prompt. Same task we started with — but now every decision the agent was about to make for me is made by me.
What I typed the first time:
add rate limiting to the API
What I should have typed:
We need rate limiting on the public API.
Constraints:
- Per API key, not per IP — we're behind a load balancer
- 100 req/min standard, 1000 for partner keys
- We run 3 replicas, so the counter has to be shared
- On breach: 429 with Retry-After. Never queue.
- Internal service-to-service calls are exempt
Out of scope:
- Don't touch the auth middleware
- Don't add infrastructure without asking me first
- Don't reformat files you only needed to read
Done when:
- tests/test_rate_limit.py passes
- the 101st request within a minute returns 429
- docker compose up still boots clean
Before you write any code: tell me what's ambiguous here,
and what you'd change about this approach.
Longer to write? About ninety seconds. But look at what it does: the five hidden decisions are now explicit, the Redis question is surfaced as a question instead of silently answered, drift is fenced off, "done" is a command you can run, and the last line still forces the interview — because I might be wrong too, and I'd rather find out now.
That last line matters more than it looks. A plan that can't be challenged isn't a plan, it's an instruction. The whole point of the harness is an agent that pushes back when it has a reason to — so give it the chance before it starts typing, not after.
What changes when it's fifty developers
Everything above is worth doing alone in a side project. In an organisation, it stops being a productivity habit and becomes governance.
The plan becomes the audit artifact. In regulated environments — mine is critical infrastructure, so NIS2 and the EU AI Act sit on top of everything — "why was this change made, and who decided?" needs an answer. A committed plan file, reviewed before the work started, is a real answer. A chat transcript on someone's laptop is not.
Review the plan, not just the diff. This is the part I'd argue hardest for. A tech lead cannot read every line produced by five engineers each running an agent — but they can read five plans, before any code exists, when changing direction still costs nothing. A wrong approach caught in a plan costs a five-minute conversation; caught in review it costs a day; caught in production it costs a weekend. The cheapest review in the whole pipeline happens before a single line is generated, and almost nobody does it.
Where you catch a wrong approach decides what it costs
Same mistake, three moments of discovery — time to recover, in minutes
Data as table
| Caught | Typical cost | Minutes |
|---|---|---|
| In the plan | a conversation | ~5 |
| In code review | rework | ~480 |
| In production | incident + fix | ~2,880 |
It makes agent work reviewable by someone who wasn't there. This is the quiet one. Code produced in a two-hour session with an agent carries a huge amount of unwritten context. The plan is where that context gets written down — so the reviewer, and you in six months, aren't reverse-engineering intent from a diff.
When not to plan
I'd rather be honest than sell you ceremony. Planning has a cost, and there are cases where it's pure overhead:
- Changes you can predict. If you already know what the diff will look like, you don't need a plan — you need the diff. Fixing a typo, bumping a version, renaming a variable.
- Exploration and spikes. When the point is to find out whether something is even possible, planning is premature. Let it be messy, then throw it away — genuinely throw it away, don't promote the spike to production.
- Anything you'd be happy to delete. Throwaway scripts don't need process.
The tell is simple: if you can predict the diff, skip the plan. If you can't, that uncertainty is exactly what the plan is for.
The Bottom Line: An agent will never tell you your request was ambiguous — it will quietly decide what you meant and build on it. Ten minutes spent making it interview you, writing the plan to a file, and naming what's out of scope buys back the two hours you'd otherwise spend reviewing code you never asked for. The agent writes the code; the decisions were always yours to make.
Next in the series: Prompting Is Context Engineering — what to feed the agent, and what to withhold.