Working inside Cursor / Claude Code / Aider / Copilot

Cursor: the diff envelope

4 min read

When you're prompting an LLM inside an IDE — Cursor, Zed, IntelliJ AI Assistant, JetBrains AI — the model isn't free to chat. It needs to produce output the IDE can parse and apply directly to your buffer. The right output shape is a unified diff wrapped in a code block.

This module's flow:

Module 5 — one skeleton, five output envelopes

1. Cursor

Unified diff the IDE applies to your buffer

2. Aider

SEARCH/REPLACE — exact text, no line numbers

3. Claude Code

Plan, approve, then execute — with a real permission gate

4. Copilot

No prompt at all. The file above the cursor is the prompt

5. Commit message

Conventional commit generated from the diff

Cursor's user-message → diff round-trip:

The round-trip, and the two places it can break

locks the envelopesupplies the taskSystem prompt'Unified diff only. Preserve un…User messageThe file, then the change reque…Model emits a diff---/+++ headers, @@ hunk header…YOUSemantic checkDoes the new code do the right …TOOLINGStructural checkDo the @@ line counts match the…Applied to the bufferBoth checks passed, so the chan…

A short system prompt + a focused user request gets you there. Here's the system prompt that makes the model behave like an IDE-native pair:

You are an expert pair programmer working inside an IDE. The user shows you code; you respond with a unified diff (--- / +++ / hunk markers) only — no prose unless explicitly asked. Always preserve unrelated lines exactly.

Now the user message is just the file and the change request:

File: src/utils/email.ts

export function isValidEmail(s: string): boolean {
  return s.includes("@") && s.includes(".");
}

Task: tighten the validation. Reject leading/trailing whitespace, require at least one char before "@", require a dot AFTER "@", and a TLD of at least 2 letters. Keep the same signature.

Claude Sonnet 4.5 returned:

--- src/utils/email.ts
+++ src/utils/email.ts
@@ -1,3 +1,11 @@
 export function isValidEmail(s: string): boolean {
-  return s.includes("@") && s.includes(".");
+  if (s !== s.trim()) return false;
+  
+  const atIndex = s.indexOf("@");
+  if (atIndex <= 0) return false;
+  
+  const domain = s.slice(atIndex + 1);
+  const lastDotIndex = domain.lastIndexOf(".");
+  if (lastDotIndex === -1) return false;
+  
+  return domain.slice(lastDotIndex + 1).length >= 2;
 }

Captured from Claude Sonnet 4.5 (claude-sonnet-4-5) on 2026-04-27. Re-runs may differ slightly. See the model-lifecycle note in Module 1, lesson 1.

The logic is right. Every case in the task passes. And this diff does not apply. Save it to a file and run it:

$ git apply -p0 email.diff
error: corrupt patch at line 17

$ patch -p0 < email.diff
patch: **** malformed patch at line 16:  }

Read the hunk header: @@ -1,3 +1,11 @@. It claims the replacement spans 11 lines. Count the hunk body — one context line, ten + lines, one closing } — and it is 12. Both git apply and patch verify that count before touching your file, and both refuse.

This is the single most useful thing in the lesson, so do not skim past it. A unified diff carries two kinds of correctness, and they fail independently:

LayerWhat it meansHow it failsWho catches it
SemanticDoes the new code do the right thing?Wrong logic, wrong edge caseYou, in review
StructuralAre the hunk offsets and line counts internally consistent?Off-by-one line count, wrong @@ headerThe patch tool, before it writes anything

A model that is good at the first is not automatically good at the second, because the second is arithmetic performed over its own output — it has to count the lines it is about to emit before it emits them. That is precisely the kind of bookkeeping language models are weakest at, and it is invisible when you read the diff, because your eye reads the code and skips the @@ line.

Change +1,11 to +1,12 and the same diff applies cleanly. Nothing else needs to move.

What to do about it, in order of preference:

  1. Let the tool count. Cursor and most IDE assistants apply model edits through a fuzzy matcher that re-anchors on surrounding context rather than trusting the header, so the broken header often survives inside the IDE and then fails the moment you export the diff, paste it in a PR, or pipe it to git apply. The envelope you can hand to another tool is the one that has to be right.
  2. Ask for whole functions, not hunks, when the change is small. Output the complete rewritten function in a single code block has no offsets to get wrong.
  3. Verify before you trust. git apply --check is instant and exits non-zero on a bad patch:
git apply --check my-change.diff && echo "applies cleanly"

Add that one line to your loop and structural errors stop reaching your branch.

Semantically, the output is clean: no prose to strip, no explanation to ignore, no alternative implementations. That is what the format constraint bought. It just did not buy a diff that applies.

The system prompt is doing four things at once:

PhraseEffect
"expert pair programmer"Sets the persona toward concise senior-engineer voice
"working inside an IDE"Frames the output for tool consumption
"unified diff only"Locks the output format
"preserve unrelated lines exactly"Stops the model from sneaking in formatting changes

That last constraint matters more than it looks. Without it, the model often "improves" indentation, changes single quotes to double quotes, or adds a missing trailing newline. Each of those creates noise in the diff and in the eventual git history. The constraint forces the diff to contain only your intended change.

A useful extension: ask the model to also add the test in the same diff:

Task: tighten the validation as described, AND add a unit test in src/utils/email.test.ts covering the new rules. Both changes in the same diff.

The diff envelope handles multi-file changes naturally — each file gets its own --- / +++ pair. The IDE applies all hunks together, and you get an atomic change.

When the diff comes back wrong, the fix is rarely "rerun with a better prompt." It's usually "show me what you tried, and apply the patch I describe." Cursor, Zed, and similar tools let you do this through their chat surface. The conversation pattern: model proposes diff → you apply mentally → you correct one specific line → model rewrites the diff. Three turns, one merged change.

The four IDE-style envelopes you'll meet across this module, side by side:

IDE-style prompt envelopes

Diff

Cursor / Zed

Input shapeFile + task in chat
Output shapeUnified diff (---/+++)
Apply stepIDE patches buffer
Strong atInline edits, multi-file hunks
Pros
  • Atomic multi-file change
  • Reviewable hunk by hunk
Cons
  • Line-number drift breaks stale diffs
  • Tempts model to re-format unrelated lines
SEARCH/REPLACE

Aider

Input shapeRepo context + task
Output shape&#x3C;&#x3C;&#x3C; SEARCH / === / REPLACE >>>
Apply stepAider matches exact text
Strong atLong sessions, line-shift safety
Pros
  • Survives surrounding edits
  • Apply fails loudly on misread
Cons
  • One block per logical edit or rollback gets ugly
  • Verbose for tiny one-line changes
Plan + execute

Claude Code

Input shapeConversational task + repo
Output shapePlan → tool calls → diff
Apply stepAgent edits + runs tests
Strong atMulti-step features, invariants
Pros
  • Plan-then-go gate prevents scope creep
  • Agent self-checks invariants
Cons
  • Long context required
  • Failure mode is silent file edits without 'go'
Completion

Copilot / Tabnine

Input shapeFile above cursor
Output shapeInline completion
Apply stepTab to accept
Strong atFunction bodies given good signature
Pros
  • Zero-friction in flow
  • Improves with comments above cursor
Cons
  • No control surface beyond the file
  • Generic if function name is vague

Next up: the SEARCH/REPLACE block format used by Aider. :::

Quiz

Module 5: IDE & Tool Prompts

Take Quiz
Was this lesson helpful?

Sign in to rate