Context engineering for coding agents
What to put in an agent's context window and what to keep out: project instructions, retrieved code, the task spec, and history you barely control.
Context engineering is the part of working with coding agents that people do badly for the longest, because the failure mode is invisible. Prompt engineering has a feedback loop: bad prompt, bad output, fix prompt. Context engineering fails quietly. The agent produces something plausible built on a partial or misleading picture, and you find out three files later.
The whole discipline reduces to one decision made repeatedly: what does the agent see when it starts thinking about this? Everything below is that question applied to four layers, each with different economics.
The four layers
Roughly in order of how much control you have:
| Layer | What it is | Your control |
|---|---|---|
| Project instructions | CLAUDE.md, AGENTS.md, system prompt additions | Total |
| Retrieved code | What the agent greps, reads, and pulls in | High, indirect |
| Task specification | The thing you're asking for | Total |
| Conversation history | Everything already said and done | Almost none |
Most effort goes into layer one because it's the most controllable. Most value is in layers two and three. And layer four — the one you control least — is where things actually break down.
Project instructions: short, imperative, and about invariants
A CLAUDE.md (or equivalent) is the only context that's guaranteed present in every session. That makes it valuable and makes it tempting to overfill.
What belongs: things that are true across every task and that the agent would otherwise get wrong.
- Non-obvious tooling constraints. "Use pnpm only. No npm/yarn/bun lockfiles." An agent will reach for
npm installby default; one line prevents it forever. - Architectural invariants with a blast radius. "Zero schema, queries, and mutators live only in
packages/zero." Violating this produces code that works locally and breaks the sync layer in a way that's hard to trace back. - Things that fail loudly and are easy to get wrong. "Postgres and zero-cache must both be running before the frontend shows data."
- Explicit prohibitions with consequences. "Never
npm publishby hand — publishing goes through the pipeline only."
What does not belong:
- Anything the agent can discover by reading. Don't describe what a file does; the agent will read the file. Directory structure is borderline — a compact tree helps navigation, but a paragraph per directory doesn't.
- Style preferences the linter enforces. If ESLint catches it, the instruction is redundant; the check is the enforcement.
- Task-specific detail. That's the spec's job.
- Aspirations. "We value clean code" consumes tokens and constrains nothing.
The length problem is real. A 500-line instruction file gets partially ignored, and this is worth being precise about because the usual explanation is wrong. It's not that the model can't read 500 lines — it obviously can. It's that instruction files are read once at the start and then compete with everything that arrives after. By the time the agent is 40 minutes into a task with 50k tokens of file reads and tool output between it and your instructions, an instruction stated once on line 340 has very little weight against the pattern it just saw in three files it read five minutes ago.
Nearby, concrete, recent evidence beats distant, abstract instruction. Every time.
The practical consequence: put invariants where they'll be re-read, not just where they're stated once. An instruction file entry saying "never use any" is weaker than an ESLint rule that fails the build, which is weaker still than the surrounding code containing no any for the agent to pattern-match against. Ranked by actual effect:
- A check that fails (typecheck, lint, test). The agent hits it and self-corrects, in-session, with the failure in immediate context.
- A comment in the file where the constraint applies. Read at the moment of relevance.
- A line in the project instruction file. Read once, at the start.
This inverts the usual instinct. When an agent keeps violating a rule, the reflex is to restate the rule more emphatically in the instructions. Adding capitals and "IMPORTANT" to line 340 helps marginally. Moving the constraint into a failing check helps categorically. The instruction file is the weakest enforcement mechanism you have — use it for things that can't be checked mechanically, not as a substitute for checking.
A useful sizing rule: if you can't read your instruction file top to bottom in two minutes, the agent is effectively working from a subset of it, and you don't get to choose which subset.
Retrieved code: relevance over volume
The second layer is what the agent pulls in while working — grep, read, follow-the-import.
Periodically someone suggests just putting the whole repo in the window, and with large context windows it's technically possible for small repos. It's a bad idea, and the reason isn't cost.
Irrelevant context is not neutral. It is harmful.
This is the single most important thing to internalize about context engineering, and it's counterintuitive because we're used to thinking of more information as weakly positive. It isn't, for two reasons.
First, attention dilutes. The model distributes attention across what's present. Fifteen files where three are relevant means the signal from those three competes with twelve files of noise. The relevant detail is still in there, but it's less likely to drive the output.
Second — and this is the one that actually costs you — the agent will act on what it sees. Show it a file with a latent bug, an awkward abstraction, or an inconsistent naming convention, and there's a real chance it "helpfully" fixes that too. You asked for a comment feature and got a diff that also refactors an unrelated utility, because the utility was in context and looked improvable.
Scope sprawl is frequently a context problem, not an obedience problem. The agent isn't ignoring you; it's responding to what's in front of it.
So: grep and read, don't dump. The workflow that works is search by name, read what matches, follow imports one hop, stop. This is also why a repo where relevant code is findable by name is worth real effort. If packages/zero/ contains all the Zero code and nothing else does, an agent asked about sync finds the right files in one grep. If sync logic is scattered across utils/, helpers/, and lib/common/, it finds three of five places and confidently writes code against an incomplete picture.
Concrete things that help:
- Names that match domain concepts. A file called
specVersions.tsis discoverable;manager2.tsis not. - Colocation by feature, not by technical layer. Feature-based structure means one grep gets the whole vertical. Layer-based structure means five greps and you'll miss one.
- No parallel implementations of the same concept. Two auth paths means the agent finds one, and it's a coin flip which.
- Deleted dead code, actually deleted. Commented-out blocks and
oldHandler.tsare pure context poison — they look like real code and pattern-match as strongly as real code.
This is a case where structuring a codebase for agents and structuring it for humans converge almost completely. Discoverable-by-name is what you wanted anyway.
The task specification: the layer with the best return
The third layer is what you actually asked for, and it's where marginal effort pays best — partly because it's the layer most people underinvest in relative to fiddling with instruction files.
The failure isn't vagueness exactly. It's unstated constraints that the agent has no way to infer. An agent given "add comment deletion" will produce something reasonable. Whether it soft-deletes or hard-deletes, whether it cascades to replies, whether non-authors with admin rights can delete — it will decide all of these, silently, and you'll discover the decisions in review.
Each unstated constraint is a coin flip. A spec with six unstated constraints is six coin flips, and the probability all six land where you wanted is not good.
The economical version of this: write down the decisions, not the implementation. You don't need to specify the SQL. You need to specify that deletion is soft, that replies survive, and that admins can delete anyone's comment. That's three sentences that remove three coin flips. This is what writing specs for coding agents is actually about — not more words, but the specific words that close off wrong branches.
Second-order effect worth knowing: a precise spec reduces how much the agent needs to retrieve. An agent that knows the deletion semantics doesn't need to read five files hunting for precedent. Layer three done well shrinks layer two, which leaves more room for reasoning. The layers trade against each other, and spec tokens are the highest-value tokens in the window.
Conversation history: the layer you don't control
The fourth layer accumulates whether you like it or not. Every file read, every tool result, every failed command, every intermediate reasoning step.
The problem isn't length per se. It's that history degrades unevenly. Early in a session, everything in context is deliberate — instructions, spec, files you pointed at. Two hours in, most of the window is tool output, half of it from paths that turned out to be dead ends. The signal-to-noise ratio has dropped and nothing corrected for it.
Then compaction kicks in. Compaction is lossy in a specific, unhelpful way: it preserves what looks like narrative — what was done, in order — and drops what looks like detail. But the detail is often where the constraints live. The exact function signature, the specific reason an approach was rejected, the edge case you called out an hour ago. What survives is "we implemented the comment feature." What's lost is "we chose soft delete because of the audit requirement."
Which is how you get the classic post-compaction failure: the agent re-derives a decision it already made, differently. It isn't contradicting itself from carelessness — it's reasoning correctly from a summary that dropped the constraint. Why agents lose context covers the mechanism in more depth.
The practical response is to start fresh sessions at natural boundaries rather than compacting a degraded one. A fresh session with the spec, the instruction file, and a pointer to the relevant files is almost always in better shape than a compacted session carrying two hours of sediment. It costs you a few minutes of re-orientation and buys back a clean window.
Natural boundaries are usually obvious: a task finished. An approach abandoned. A long debugging detour concluded. Anything where the next thing you do doesn't depend on the fine detail of the last thing.
The anti-pattern is compacting to continue in-flight, mid-task, because you're close to done. That's exactly when the dropped detail matters most, and it's exactly when you're least inclined to notice.
If you must carry state forward, carry it explicitly: write the decisions into a file — a spec, a scratch note, a comment — before ending the session. Written-down state survives compaction because it can be re-read. State that only exists in the transcript does not.
What's still unsolved
Being straight about the limits, because the field has more confident advice than it has earned.
Nobody knows the right instruction file length. "Short" is directionally right and unfalsifiable. The real answer depends on model, task, and how much other context accumulates, and there's no way to measure it from the outside. You find out something was ignored by watching it get ignored.
Retrieval quality is still mostly manual. Semantic search over code helps but does not solve it — the thing an agent needs is often related conceptually rather than lexically or semantically, like a config value in a completely different file that constrains the code being changed. Humans find it by knowing the system. Agents find it by luck or by being told.
There is no good way to tell whether context is sufficient before acting. An agent with a partial picture produces confident output indistinguishable in tone from an agent with the full picture. This is the deepest problem in the space and nothing currently addresses it. You find out afterward, in review, which is why reviewing AI-written code remains load-bearing rather than optional.
Multi-session state has no real solution. Everything above is about one session's window. Across sessions and across people, the durable state lives in code, in git history, and in whatever you wrote down. Written specs are the best available answer and they drift from the code the moment either changes. That drift is a genuine open problem, not one that's been quietly solved by tooling.
The honest summary: context engineering is currently a craft with reliable heuristics and no measurement. Keep instructions short and imperative. Put invariants where they get re-read. Retrieve narrowly. Spend your best tokens on the spec. Start fresh instead of compacting. Then verify the output anyway, because none of it is guaranteed.
Keep reading
- Planning mode vs. just promptingWhen making an agent plan before it writes code pays for itself, when it is pure ceremony, and what separates a useful plan from a restated prompt.
- Scoping work for AI agentsHow to size a task so a coding agent can finish it coherently: decision count over line count, vertical slices over layers, and dependency order.
- Drift between the spec and the codeThe doc says one thing, the code does another, and nobody notices until an incident. Why agents make it worse, and what you can actually detect.