The Real Problem: AI Tools Go Blind Past 10 Files
Most tutorials show an AI coding assistant completing a 40-line function in a single-file project. That demo looks convincing. Then you open the assistant in your actual codebase, 400,000 lines across 600 files, three years of decisions baked in, a custom RPC layer nobody documented, and the output is almost insultingly wrong. It hallucates module paths. It reinvents abstractions you already have. It writes code that would fail the linter on line two.
The gap is not the model's fault. The gap is context. The model only knows what you showed it. On a large codebase, you showed it almost nothing.
Context engineering is the deliberate practice of deciding what information goes into an AI tool's context window, in what form, and at what point in your workflow. It is the difference between an assistant that accelerates you and one that slows you down with confident, plausible garbage.
This post goes through the mechanics of that practice. It covers how modern AI coding tools manage context, where they break down at scale, and concrete patterns you can apply today in TypeScript, Python, or any polyglot codebase. Code examples are minimal and correct. Concepts are grounded in how tools like Claude Code, Cursor, Copilot Workspace, and Aider actually work in 2025 and 2026.
How Context Windows Actually Work in Coding Tools
Before you can manage context, you need a mental model of what fills it.
Every AI coding tool ultimately sends a payload to an LLM. That payload has a hard size limit. As of mid-2026, frontier models offer 200K tokens (Claude 3.x, Claude Sonnet 4) or 128K tokens (GPT-4o). One token is roughly 0.75 words of English, or around 3.5 characters of dense TypeScript. A 200K-token window fits about 150,000 words, which sounds large until you realise a single mid-sized feature file plus its imports, types, tests, and a few related services can easily consume 30,000 tokens.
The context payload typically has four layers:
-
System prompt. The tool vendor's instructions, coding conventions, and tool descriptions. You rarely control this directly, but some tools (Claude Code, Aider) let you prepend your own via a project-level file.
-
Conversation history. Every message in the session. Long sessions fill up fast. A single round-trip where the model reads two files and you reply might cost 6,000 tokens.
-
Retrieved content. Files the tool fetched automatically, via grep, directory listing, or a RAG step. Quality varies enormously between tools.
-
Your prompt. Usually the smallest piece, but the most controllable.
The retrieval step is where large codebases break down. A tool doing naive file retrieval (searching for the class name you mentioned, then pulling the first three files it finds) will miss the context that actually matters. Good context engineering patches that gap manually.
The Three Context Failure Modes
Understanding failure modes helps you decide which fix to apply.
Stale context. The model sees an old version of a file because the tool cached it, or because you showed it manually early in the session and the file changed since. This produces subtly wrong completions: valid-looking code that calls an API that was renamed six months ago, or uses a pattern the team deprecated.
Sparse context. The model sees the call site but not the implementation, or the type definition but not how it is actually constructed at runtime. This produces confident guesses. The model knows the type is PaymentIntent, but it does not know your codebase wraps Stripe's PaymentIntent in a domain object with extra validation.
Polluted context. Too many files go in. The model's attention spreads thin. On tasks that need surgical precision, broad context actively hurts. Several published evals (including Anthropic's needle-in-a-haystack benchmarks) show accuracy degrading when relevant information is buried in the middle of a long context rather than near the top or bottom.
Each failure mode has a different fix. Stale context needs freshness signals. Sparse context needs targeted retrieval. Polluted context needs pruning.
Targeted Retrieval: Giving the Model the Right Slice
The default tool behaviour is to include whatever is convenient. Your job is to include whatever is necessary.
A useful mental model: think of your codebase as a graph where nodes are modules and edges are imports. The relevant context for any task is usually a two-hop neighbourhood around the file you are editing, filtered to the parts that share data shapes with your change.
In practice, that means gathering:
- The file you are editing
- Its direct imports (one hop)
- The type definitions those imports expose (usually a second hop)
- Any tests that exercise the path you are changing
- The relevant interface or schema file if your change crosses a boundary
Here is a small script that produces this slice for a TypeScript project and dumps it to a file you can paste directly into Claude Code or Cursor:
#!/usr/bin/env bash
TARGET="$1"
OUT="context-slice.txt"
> "$OUT"
echo "=== TARGET ===" >> "$OUT"
cat "$TARGET" >> "$OUT"
echo -e "\n=== DIRECT IMPORTS ===" >> "$OUT"
grep -E "^import .+ from ['\"]\./" "$TARGET" \
| sed "s/.*from ['\"]//;s/['\"].*$//" \
| xargs -I{} bash -c 'f=$(ls {}.ts {}.tsx 2>/dev/null | head -1); [ -n "$f" ] && echo "--- $f ---" && cat "$f"' \
>> "$OUT"
echo -e "\n=== RELATED TESTS ===" >> "$OUT"
BASENAME=$(basename "$TARGET" .ts)
find . -name "${BASENAME}.test.ts" -o -name "${BASENAME}.spec.ts" 2>/dev/null \
| head -3 \
| xargs -I{} bash -c 'echo "--- {} ---" && cat "{}"' \
>> "$OUT"
echo "Context written to $OUT ($(wc -w < $OUT) words)"You can extend this with ts-morph for a proper AST-based import walk rather than regex, which handles re-exports and barrel files correctly.
The key point is that you are doing deliberate retrieval, not hoping the tool picks the right files. On a 400,000-line codebase, the tool cannot know that processCharge.ts depends on a singleton StripeClient configured in src/infra/stripe.ts three directories away, unless you tell it.
Writing a CLAUDE.md (or Tool-Equivalent) That Actually Scales
Claude Code reads a CLAUDE.md file from the project root. Cursor has .cursorrules. Aider uses .aider.conf.yml. These are your lever for injecting persistent context that every session inherits without you repeating it.
Most engineers write a CLAUDE.md once and forget it. The result is a file that was accurate on day one and misleading by month three. Treating it like living documentation (update it when architecture changes, review it in sprint retrospectives) compounds its value dramatically.
Here is what a well-structured CLAUDE.md looks like for a mid-sized Next.js + tRPC + Postgres codebase. This is not a template; it is a worked example with the reasoning behind each choice:
## Architecture in one paragraph
Next.js 15 App Router frontend. tRPC v11 for all client-server calls (no REST endpoints except /api/health).
Postgres 17 via Drizzle ORM. No raw SQL except in db/migrations/. Auth is NextAuth v5 with the database adapter.
All AI calls go through server/ai/client.ts (Anthropic SDK, Sonnet 4 by default, Haiku for cheap batch ops).
## Absolute rules
- Never import from server/ in app/components/ or app/page.tsx. Use tRPC hooks only.
- Never call the Anthropic SDK directly from a route handler. Use server/ai/client.ts.
- Types live in types/. Do not inline complex types in component files.
- All DB access goes through db/queries/. No Drizzle calls outside that directory.
## Common traps
- `userId` in session is a string UUID, not a number. Postgres stores it as uuid, not serial.
- The `resume` table has a soft-delete column `deletedAt`. All queries must filter WHERE deleted_at IS NULL.
- tRPC context is set up in server/trpc/context.ts. The session object shape differs from NextAuth's default.
## What to check before adding a new feature
1. Does a similar query already exist in db/queries/?
2. Is the route protected? Check middleware.ts allowlist.
3. Does this need a DB migration? Run `pnpm drizzle-kit generate` and commit the output.Notice what this file does not contain: it does not list every file in the project, it does not repeat the README, and it does not describe the tech stack in marketing language. It answers the questions a capable engineer would have on day two, not day one.
Keep it under 600 words. Past that, important rules get buried in the middle of the context where attention is weakest (the same vulnerability as the needle-in-a-haystack failure mode described earlier).
Prompt Patterns That Change What the Model Attends To
Even with good background context, how you phrase the prompt shapes what the model focuses on. Three patterns make a material difference on complex codebases.
The constraint-first prompt. State the constraints before the goal. This forces the model to hold your architectural rules in working attention while it plans the implementation.
Bad:
Add a new tRPC endpoint that fetches all resumes for a user.Better:
Add a tRPC query in server/trpc/routers/resume.ts that fetches all resumes for the current user.
Constraints:
- Query goes through db/queries/resume.ts, not inline in the router.
- Filter soft-deleted rows (deleted_at IS NULL).
- Input: none (userId comes from ctx.session).
- Output: array of { id, title, updatedAt }.The second prompt produces code that fits your architecture on the first attempt. The first prompt produces code you have to fix three times.
The show-your-assumptions prompt. For complex refactors, ask the model to state its assumptions about the codebase before it writes code. This surfaces wrong beliefs before they are embedded in 80 lines of TypeScript.
Before writing any code, list the assumptions you are making about:
1. How PaymentIntent is currently constructed
2. Where Stripe webhook verification happens
3. What the existing error handling pattern is in this service
Then write the refactor.This adds one round-trip but saves multiple. The model's stated assumptions are usually a reliable signal of where its context is thin.
The diff-first prompt. For changes to existing code, ask for a minimal diff rather than a full rewrite. Full rewrites destroy context about why the original code was written that way. Minimal diffs are also cheaper to review and easier to reject if wrong.
Show only the changed lines as a unified diff. Do not rewrite unchanged sections.Failure Mode in Detail: The Confident Wrong Refactor
Here is a real pattern that trips up even experienced engineers. You ask an AI tool to refactor an authentication flow. The tool has seen auth.ts and session.ts but not middleware.ts. It produces clean, correct-looking code that removes the session check from auth.ts because it can see the check is duplicated, but it does not know that middleware.ts only runs the check on protected routes and the check in auth.ts is the fallback for routes that bypass middleware.
The code ships. Auth on a subset of API routes breaks. It takes two hours to find the cause because the code is otherwise correct.
The fix is simple in retrospect: before any refactor, run a grep for all call sites of the function being changed and add the top three to the context.
grep -rn "checkSessionValidity" src/ --include="*.ts" | head -20Feed those file snippets into the context explicitly. The model cannot know what it was not shown.
RAG for Codebases: When It Helps and When It Hurts
Several tools (GitHub Copilot Workspace, Sourcegraph Cody, Continue.dev) use retrieval-augmented generation to automatically fetch relevant code. The embedding model converts your query and candidate files into vectors and returns the closest matches. In theory this solves the retrieval problem automatically.
In practice, RAG on code has a specific failure mode: semantic similarity is not the same as dependency. UserProfileCard.tsx is semantically similar to UserAvatar.tsx (both are about user UI), but if your task involves changing the shape of the User type, the relevant context is types/user.ts, db/schema/users.ts, and server/trpc/routers/user.ts, none of which contain words similar to your query.
Use RAG tools as a starting point, not a complete solution. They are excellent at finding similar patterns (how did we handle pagination last time?) and poor at finding structural dependencies (what else breaks if I change this type?). Combine them with explicit import-based retrieval for structural tasks.
For teams managing this at scale, a lightweight code-search tool like Sourcegraph or a local ctags index gives you precise dependency lookup that pure embedding search cannot match. The two approaches are complementary, not alternatives.
How to Apply This in Practice: A Daily Workflow
Theory is cheap. Here is how context engineering actually fits into a daily engineering workflow without adding significant overhead.
Session start. Before opening the AI tool, spend 60 seconds identifying the three to five files most relevant to today's task. This is the two-hop neighbourhood from the target file. Open them manually in the tool or add them to the context explicitly. Do not rely on the tool to discover them.
Per-task prompt discipline. Use constraint-first prompts for any task that touches more than one module. Use show-your-assumptions for any refactor. Use diff-first for changes to existing, working code.
Context hygiene during long sessions. Most tools do not automatically drop old messages from context; they truncate from the oldest end. After four or five exchanges, the files you showed at the start of the session may have been truncated. Re-add critical files when starting a new subtask.
CLAUDE.md update cadence. When you merge a pull request that changes a core pattern (new auth mechanism, new query convention, new service boundary), update CLAUDE.md as part of the PR. Treat it like a changelog for architectural decisions. Two sentences per change is enough.
Pre-commit AI review. Run a final AI pass on the diff before pushing, with the prompt: "Does this diff violate any of the constraints in CLAUDE.md? Does it introduce any patterns inconsistent with the surrounding code?" This catches the most common class of AI-generated mistakes: valid code that does not fit the existing system.
git diff HEAD --staged | claude --print \
"Review this diff against CLAUDE.md constraints. List any violations. Be terse."Tradeoffs and Honest Limits
Context engineering adds overhead. The friction is real: writing good CLAUDE.md entries takes time, explicit retrieval scripts need maintenance, constraint-first prompts take longer to write. Whether the investment pays off depends on how often you use AI tooling and how large your codebase is.
For a single-person project under 20,000 lines, the default tool behaviour is probably good enough. The gap between what the tool retrieves and what it should retrieve is small, and the cost of occasional wrong output is low.
For a team codebase above 100,000 lines, context engineering is not optional. It is the difference between AI tooling that accelerates the team and AI tooling that produces a steady stream of plausible bugs that slow down code review.
The hardest part is not the technique; it is the habit. Engineers who get the most out of AI tooling are the ones who treat the context payload with the same care they give to a pull request description. They think about what the reader does not know and tell them. The model is the reader.
One honest limit: no amount of context engineering fixes a model that simply does not know a library, framework version, or internal convention. For highly proprietary internal tooling, fine-tuning or few-shot examples in the system prompt are more effective than retrieval. Context engineering is a retrieval and framing technique; it cannot add knowledge the model does not have.
Key Takeaways
- Context engineering is the practice of deliberately controlling what an AI tool sees, not hoping it figures it out.
- The three failure modes are stale context, sparse context, and polluted context; each has a different fix.
- A well-maintained CLAUDE.md (or tool equivalent) under 600 words pays compounding returns over the life of a project.
- Constraint-first prompts, show-your-assumptions prompts, and diff-first prompts each solve a specific class of bad output.
- RAG retrieval is good at semantic similarity, poor at structural dependency; combine it with explicit import-based retrieval.
- The confident wrong refactor is the most dangerous failure mode: always grep for call sites before refactoring a function.
- At scale, treating AI context with the same care as a PR description is what separates teams that accelerate with AI from teams that clean up after it.