#Give the model the context it needs
AddedA prompt for code is a ticket for someone who has never seen your repository and can't ask you questions until they hand in the work. Everything the model needs to get it right has to be in the context: which files to change, the types and helpers to use, the constraints (versions, no new dependencies, error format), an example of the expected result and what "done" means. Whatever you leave out, it fills in with the most common version of that thing from its training data, which is a reasonable guess about the average codebase and a poor one about yours (see How they work, just enough to use them well).
The mental model is the contractor's ticket. "Fix the login bug" works with a colleague who knows the system and can lean over to ask. A contractor needs: the symptom, where to look, what fixed looks like, and what not to touch. The vendors' own guidance is the same list. GitHub's "Prompt engineering for GitHub Copilot" page (checked 2026-10-27) says to start general then get specific, give examples, break complex tasks into simpler ones, avoid ambiguity, provide relevant context ("Copilot uses the context available to it to understand your prompt"), iterate, and keep history relevant. Claude Code's best-practices page puts it as: "If you name the file, describe the symptom, and say what 'fixed' looks like up front, you usually save yourself a round of corrections later", with examples like these:
| Instead of | Try (from Claude Code's best-practices page, checked 2026-10-27) |
|---|---|
| "fix the login bug" | "users report login fails after the session times out. look at token refresh in src/auth/, write a failing test that reproduces it, then fix it" |
| "add a calendar widget" | "follow the pattern in HotDogWidget.php and build a calendar widget that lets you pick a month and page between years. no new libraries" |
What goes into a good code prompt, and why each part is there:
| Part | Example | What it prevents |
|---|---|---|
| Task and goal | "add a percentage discount to invoices" | solving a different problem |
| Where | the file and function to change; the files to read | edits in the wrong place, a duplicate helper |
| Types and data shapes | the interface, a sample payload | invented field names and units |
| Existing helpers and patterns | "use roundCents", "follow HotDogWidget" | a second, slightly different implementation |
| Constraints | versions, "no new dependencies", error format, performance limits | stale APIs, surprise packages |
| Examples | one input with its expected output | ambiguity about rounding, ordering, formats |
| Definition of done | the test to pass, the command to run, the evidence to show | "looks done" |
Here's a prompt for a small change in a TypeScript billing module, written with those parts. It's shown as you'd type it into a chat panel or an agent; the response is discussed below, not quoted, because no AI tool was run for this page.
Add a percentage discount to invoices.
Change applyDiscount(inv, pct) in src/billing/discount.ts (attached). Read first:
- src/types/invoice.ts (attached): amounts are integer cents; a line is { sku, cents, qty }.
- src/lib/money.ts: use roundCents for rounding; don't write a new helper.
- src/api/checkout.ts (attached) calls applyDiscount with pct from the request body.
Rules:
- pct is a number from 0 to 100 inclusive; anything else throws RangeError.
- Discount each line's total (cents × qty), round once per line, then sum.
- Don't change the Invoice type or the function's signature. No new dependencies.
- TypeScript strict mode; the project uses Node 26 and vitest.
Example: one line { cents: 1999, qty: 2 } at 15% → 3998 × 0.85 = 3398.3 → 3398 cents.
Done when: npm test -- discount passes and npx tsc --noEmit is clean.
Show me the diff and both commands' output.- The first line is the goal in one sentence, general before specific, as GitHub's guidance suggests. The model knows what success is before it reads any detail.
- "Change … Read first" names the one file to edit and the three files that matter, with what each contributes: the types (so it doesn't invent
amountor use floats for money), the helper (so it doesn't write its own rounding), and the caller (so it knowspctarrives from untrusted input and must be validated). In an agent you'd point at the paths; in a chat panel you'd attach them. - The rules are the decisions only you can make: the valid range and the error type, where rounding happens (per line, once; a classic source of one-cent mismatches), and what must not change. "No new dependencies" closes the most common surprise in AI diffs.
- The example pins down the arithmetic:
3998 × 0.85 = 3398.3, rounded to3398. One worked example removes more ambiguity than a paragraph about rounding, and it becomes the first test case. - "Done when" gives the check (tests and type checker) and asks for evidence, the commands' output. A good response to this prompt is a small diff in
discount.tsthat validatespct, maps over items withroundCents, plus a test file and the output; a response that also editsinvoice.tsor adds a package has broken an explicit rule, which makes it easy to reject.
For a change to one file, the context that matters is usually: what that file imports (the types and helpers it must use) and what imports it (the callers that break if its behaviour changes), plus its tests. This model walks a small repository's import graph to find exactly that set, which is what you'd attach or point an agent at.
// Which files should go into the prompt for a change to src/billing/discount.ts?
// Walk the import graph: what it imports (the types and helpers it must use) and who imports it (callers that break).
const repo = {
"src/billing/discount.ts": `import type { Invoice, LineItem } from "../types/invoice";\nimport { roundCents } from "../lib/money";\nexport function applyDiscount(inv: Invoice, pct: number): Invoice { /* … */ }`,
"src/types/invoice.ts": `import type { Currency } from "./currency";\nexport interface LineItem { sku: string; cents: number; qty: number }\nexport interface Invoice { id: string; currency: Currency; items: LineItem[] }`,
"src/types/currency.ts": `export type Currency = "CAD" | "USD" | "INR";`,
"src/lib/money.ts": `export const roundCents = (n: number) => Math.round(n);`,
"src/api/checkout.ts": `import { applyDiscount } from "../billing/discount";\n/* POST /checkout handler … */`,
"src/billing/discount.test.ts": `import { applyDiscount } from "./discount";\n/* 6 tests … */`,
"src/ui/theme.ts": `export const theme = { accent: "#84cc16" };`,
"src/api/health.ts": `export const health = () => ({ ok: true });`,
};
const importsOf = (file) =>
[...repo[file].matchAll(/from "(\.[^"]+)"/g)].map(([, rel]) => {
const parts = file.split("/").slice(0, -1);
for (const seg of rel.split("/")) seg === ".." ? parts.pop() : seg !== "." && parts.push(seg);
return parts.join("/") + ".ts";
});
const target = "src/billing/discount.ts";
const picked = new Map([[target, "the file being changed"]]);
const queue = [target];
while (queue.length) { // everything the target depends on, transitively
const file = queue.shift();
for (const dep of importsOf(file)) if (!picked.has(dep)) { picked.set(dep, `imported by ${file}`); queue.push(dep); }
}
for (const file of Object.keys(repo)) // direct callers and tests
if (importsOf(file).includes(target)) picked.set(file, file.includes(".test.") ? "its tests" : "calls it: check it still works");
const tokens = (f) => Math.ceil(repo[f].length / 4);
for (const [file, why] of picked) console.log(`${file.padEnd(30)} ~${String(tokens(file)).padStart(3)} tokens ${why}`);
const skipped = Object.keys(repo).filter((f) => !picked.has(f));
console.log(`left out: ${skipped.join(", ")}`);src/billing/discount.ts ~ 45 tokens the file being changed
src/types/invoice.ts ~ 48 tokens imported by src/billing/discount.ts
src/lib/money.ts ~ 14 tokens imported by src/billing/discount.ts
src/types/currency.ts ~ 12 tokens imported by src/types/invoice.ts
src/api/checkout.ts ~ 21 tokens calls it: check it still works
src/billing/discount.test.ts ~ 15 tokens its tests
left out: src/ui/theme.ts, src/api/health.tsrepois a tiny in-memory repository;importsOfreads a file's relativefrom "…"imports with a regex and resolves..and.segments into repository paths, so"../types/invoice"fromsrc/billing/becomessrc/types/invoice.ts.- The
whileloop is a breadth-first walk from the target:discount.tsimportsinvoice.tsandmoney.ts;invoice.tsimportscurrency.ts. All three are picked, each with the reason it was picked.Currencyis two hops away, and it's exactly the kind of type a model would otherwise guess (is it"cad"or"CAD"?). - The second loop finds reverse dependencies: files whose imports include the target.
checkout.tscallsapplyDiscount, so it goes in as "check it still works";discount.test.tsgoes in as the tests. theme.tsandhealth.tsare left out. Including them would cost tokens and attention for nothing; in a real repository, "unrelated files" is most of the repository.- The estimates (characters ÷ 4) show the whole relevant set is a few hundred tokens here. In real code the set is bigger, but the principle holds: the types, the helpers, the callers and the tests, not the whole folder. Agents that search the repository do a version of this themselves; with chat, it's your job.
Context isn't free: every token of noise competes with the signal (Anthropic's docs call the degradation "context rot"). Leave out, or cut down:
- Whole logs and stack traces: paste the failing test's output and the top frames of the trace, not 3,000 lines of CI log.
- Unrelated files that happen to be open (completion tools may include open tabs as context, so close the irrelevant ones).
- Old conversation: when the task changes, start a new session. GitHub: "Start a new conversation or session when you begin a new task." Two failed attempts in the history are context too, and the model keeps re-reading them.
- Secrets and personal data: API keys,
.envcontents, customer records, production logs with user data. Assume anything in a prompt leaves your machine; your company's policy decides which tools may see what (see Security, licensing and data concerns).
Some context belongs in every request: the test command, the versions, conventions the model keeps getting wrong. Rather than retyping it, put it in the repository's instruction file, which the harness prepends to every request: .github/copilot-instructions.md for Copilot (see Custom instructions and prompt files), CLAUDE.md or AGENTS.md for Claude Code. Claude Code's best-practices page has a good test for what belongs there: things the model can't learn by reading the code, such as "how to confirm work in this repo: the exact test, typecheck, and lint commands". It also warns that piling on emphasis ("IMPORTANT", capitals) "mostly makes Claude over-apply the rule"; a rule that must never bend should be enforced by a hook or a check, not by shouting.
"Write a function that formats a meeting's start time for the room display." List what the model has to guess, and what it will probably guess.
Show answer
At least: the input type, the time zone, the output format, the locale, edge cases, and where the code goes. It will guess the most common answers, which are probably not yours.
- Input: a
Date, an ISO string, epoch milliseconds? A model usually picksDateor an ISO string; if your API sends epoch seconds, every result is off by a factor of 1,000. - Time zone: the room's zone, the viewer's, or UTC? The common default is the runtime's local zone, which on a device or server is often not the room's. This is the guess most likely to be silently wrong.
- Format and locale: "9:00 AM", "09:00", "9h00"? 12- or 24-hour clocks vary by region; a display that ships to many countries needs this decided, often via
Intl.DateTimeFormatwith an explicit locale. - Edge cases: meetings starting tomorrow, all-day events, a missing start time. Without them, the generated function handles only today's normal meeting.
- Where and with what: which file, which date library (or none), which test framework. The fix for all five is the same: state them, plus one example per tricky case, and the function's signature.