Two prompts, same model, same codebase. This one produces something you throw away:
Add rate limiting to the invoice endpoints. Make it production-ready
and follow best practices.
This one produces something you merge:
Add per-API-key rate limiting to the invoice endpoints in @app/main.py.
Follow the dependency pattern already used for auth — see how
require_api_key is injected in @app/auth.py. Same shape.
Start with @tests/test_rate_limit.py — it's written and failing.
Make it pass without changing the assertions.
Limits come from env vars, like DATABASE_URL in @app/db.py.
Don't add a dependency; in-memory is fine for this step.
The second isn't more polite, more detailed, or better worded. It's better fed. Every improvement is a pointer to something that already exists in the repo.
That's the whole discipline: prompting is 10% phrasing and 90% deciding what the model can see. Part one covered planning the work. This is about loading the context you planned.
Everything here runs against the sample app in agent-workflow — see 02-context/ for the full pairs with captured output.
1. Point at files. Never paraphrase them.
The single highest-return habit. Compare:
❌ We have an auth system that checks API keys from an
environment variable and returns 401 if it doesn't match.
✅ @app/auth.py
The first is your memory of the code — lossy, possibly stale, and it silently invites the agent to invent the rest. The second is the code. In Claude Code, @ pulls the file into context; Cursor uses @, Copilot uses #file:. Same principle everywhere: a reference beats a description.
This matters more than it looks. Paraphrasing app/auth.py would lose the detail that key comparison uses secrets.compare_digest — and an agent that doesn't know that will happily write if x_api_key == key in the new code and quietly introduce a timing side-channel into a file that had none.
2. The failing test is the best prompt you'll ever write
If you can express the requirement as an assertion, do that instead of describing it:
# tests/test_rate_limit.py — written BEFORE the implementation
def test_101st_request_in_a_minute_is_rejected(client, standard_key):
for _ in range(100):
assert client.get("/invoices", headers=standard_key).status_code == 200
blocked = client.get("/invoices", headers=standard_key)
assert blocked.status_code == 429
assert "Retry-After" in blocked.headers
def test_partner_keys_get_a_higher_limit(client, partner_key):
for _ in range(100):
assert client.get("/invoices", headers=partner_key).status_code == 200
assert client.get("/invoices", headers=partner_key).status_code == 200
Then the prompt is one line:
Make @tests/test_rate_limit.py pass. Don't modify the assertions.
Three things happen at once. The spec becomes unambiguous — 429, Retry-After, tier-aware, all executable. "Done" becomes a command instead of an opinion. And the agent can no longer grade its own homework, because you wrote the exam.
That last one is the point. Ask an agent to "add rate limiting and write tests for it" and you get tests shaped around whatever it built — which is exactly the failure #22 warned about: the same intelligence writing the code and certifying it. Writing the test first breaks that loop for free.
3. Show the pattern you want copied
Agents are excellent mimics and mediocre inventors. Use that:
Add the limiter as a FastAPI dependency, following exactly how
require_api_key is wired into the routes in @app/main.py.
Don't invent a new pattern.
Without the pointer, you get a middleware, or a decorator, or a class-based approach — all defensible, none matching the three files around it. Consistency is worth more than cleverness in a codebase someone else has to read, and the cheapest way to get it is to name the exemplar.
4. Constraints are context too — especially the negative ones
Don't add a dependency for this step.
Don't touch app/auth.py — read it, don't edit it.
Don't reformat files you only opened to look at.
Every one of those exists because I've watched it happen. Negative space is real context: it removes options the model would otherwise consider reasonable.
What to withhold
Here's where most advice stops, and where the interesting half starts. More context is not better context.
Relevance beats volume
Dumping your whole src/ into the window feels thorough. It isn't. Every irrelevant file competes for attention with the three that matter, and the failure mode is quiet: the model doesn't say "that was too much," it just weights the wrong thing and hands you something plausible.
The test I use: could I explain to a colleague why each file is in the prompt? If not, it's noise.
This mirrors the rule from Building the Harness — a bloated CLAUDE.md gets ignored, so keep it short. Same physics, different scope: the always-on rules file and the per-task context both degrade when you pad them.
More context is not better context
Schematic — how output quality behaves as you keep adding context. Hover the zones.
Start fresh more often than feels natural
Long sessions accumulate abandoned approaches, corrected mistakes and stale file contents. By hour two the model is reasoning over a transcript in which half the statements are no longer true.
The tell: it starts reintroducing something you rejected earlier, or "fixing" code that's already fixed. That's not the model failing — that's you asking it to hold contradictions.
Fix: finish the task, capture what matters in a file — the plan, the decisions, project/MEMORY.md — and start clean. Context you care about should live in the repo, not in a chat window.
Don't hand it your own uncertainty as a requirement
❌ Use Redis for the shared counter. # ...when you don't actually know yet
✅ We need a shared counter across 3 replicas. What are the options,
and what does each cost us to run? Don't implement yet.
Guess in the prompt and you inherit your own guess as a fixed requirement — then spend an hour reviewing an implementation of a decision nobody actually made.
What changes at fifty developers
Everything above is craft. In an organisation it becomes policy, because context is data leaving your building.
There is a list of things that must never enter a prompt. Production credentials. Customer records. Personal data. Unreleased commercial terms. Third-party code you're not licensed to redistribute. This is not paranoia — in a regulated environment (mine is critical infrastructure, so NIS2 and the EU AI Act apply) pasting a customer table into an external model is a reportable incident, not a productivity shortcut.
Route the traffic, don't trust the etiquette. Individual discipline does not survive a deadline. The control that works is architectural: a governed model gateway that every request goes through — one endpoint, central auth, logging, redaction and cost attribution, pointed at models inside your data boundary. Then "don't paste secrets" is a reminder, not a control.
Make the safe path the easy path. If the approved gateway is slower or more awkward than someone's personal API key, people use the personal key. Every time. The gateway has to be the path of least resistance or the policy is decoration.
Sanitise the sample, not the schema. Most debugging needs the shape of the data, not the data. Three synthetic rows beat a production export, and they're faster to paste.
The five-line checklist
Before a non-trivial prompt:
- Pointed at the real files — not described them from memory
- Given the failing test, if the requirement can be expressed as one
- Named the pattern to imitate
- Stated the negative constraints — what not to touch, add, or reformat
- Removed everything I can't justify — including the last hour of dead conversation
The Bottom Line: The model reads your context, not your intentions. Point at real files instead of describing them, hand it a failing test instead of a paragraph, name the pattern you want copied — and then delete everything you can't justify, because the fastest way to improve an agent's output is usually to give it less.
Next in the series: Delegating to Sub-Agents — when a second agent pays for itself, and when it just burns tokens.