Working inside Cursor / Claude Code / Aider / Copilot

Aider: SEARCH/REPLACE blocks

4 min read

Aider is a CLI tool that pairs an LLM with your local repo. It applies model output to files using a specific format — SEARCH/REPLACE blocks — that's both human-readable and easy for a script to apply. Even if you don't use Aider, the format is a useful prompt-engineering primitive: it forces the model to commit to exact existing text before proposing the change.

The format is rigid:

filename.ext
<<<<<<< SEARCH
(exact existing lines)
=======
(new lines)
>>>>>>> REPLACE

The model has to reproduce the existing lines byte-for-byte. If it gets a single character wrong, the apply step fails. That failure is a feature, not a bug — a wrong SEARCH block tells you the model misread the file, and you'd rather know that before the change is applied than after.

Here's a small task done in this style:

Apply this change to src/server.py using a SEARCH/REPLACE block:

Goal: convert the synchronous fetch_user call into an async one and add a 5-second timeout.

Current:

def get_profile(user_id: str):
    user = fetch_user(user_id)
    return {"id": user.id, "name": user.name}

Output ONLY a SEARCH/REPLACE block in the format: src/server.py <<<<<<< SEARCH (exact existing lines) ======= (new lines) >>>>>>> REPLACE

Claude Sonnet 4.5 returned:

src/server.py
<<<<<<< SEARCH
def get_profile(user_id: str):
    user = fetch_user(user_id)
    return {"id": user.id, "name": user.name}
=======
async def get_profile(user_id: str):
    user = await asyncio.wait_for(fetch_user(user_id), timeout=5.0)
    return {"id": user.id, "name": user.name}
>>>>>>> REPLACE

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 block is paste-ready into Aider, and it's also human-readable. A reviewer can scan the SEARCH section, scan the REPLACE section, and confirm the change without running the tool.

Three things SEARCH/REPLACE forces that a unified diff doesn't:

PropertyWhy it matters
Exact existing linesThe model must read the file accurately before proposing changes
Single-anchor matchingApply fails on ambiguous matches, surfacing latent issues
No line-number dependenceThe block works even if surrounding lines have shifted

The third property is what makes SEARCH/REPLACE so robust in long-running sessions. A unified diff with line numbers becomes invalid the moment another change shifts those lines. A SEARCH block remains valid as long as the content it's searching for hasn't changed.

The Aider apply loop:

The Aider apply loop

  1. 1Model emits SEARCH/REPLACE

    It must reproduce the existing lines byte-for-byte before proposing the change

  2. 2Aider matches exact text

    No line numbers involved, so surrounding edits do not invalidate the block

  3. 3Match fails? Stop and look

    A failed match means the model misread the file. That is information, delivered before your file changed

  4. 4Applied — now run it

    Applying cleanly says nothing about whether the code works. Only execution does

The constraint to add for safety: when the change is large, ask for one block per logical unit, not one giant block:

If the change spans multiple logical edits (e.g., function signature + body + caller updates), output one SEARCH/REPLACE block per edit.

Multiple small blocks fail independently. If one fails to apply, you keep the rest. One giant block fails atomically — all or nothing — which is rarely what you want during iterative development.

Now the part worth the whole lesson. The captured output has two defects, and only one of them is the one you notice first.

The obvious one: the model used asyncio.wait_for without adding import asyncio. Run it and you get NameError: name 'asyncio' is not defined. The fix is a follow-up SEARCH/REPLACE block adding the import, or a stricter prompt: "Include any imports the change requires; if an import is missing from the file, add a separate SEARCH/REPLACE block to add it."

The one that survives that fix: add the import and run it again.

TypeError: An asyncio.Future, a coroutine or an awaitable is required

asyncio.wait_for takes an awaitable. In the file as given, fetch_user is synchronous — that is the whole premise of the task — so fetch_user(user_id) returns a plain object, and wait_for rejects it. Worse than the crash is what the code does on the way to it: Python evaluates the argument before wait_for is entered, so the blocking call runs to completion first. Even in the version of this code that does not raise, the five-second timeout would be guarding nothing.

The task said "convert the synchronous fetch_user call into an async one." The model wrapped a synchronous call in async machinery instead, which is a different change that happens to look like the requested one.

A version that does what was asked has to move the blocking call off the event loop:

async def get_profile(user_id: str):
    user = await asyncio.wait_for(
        asyncio.to_thread(fetch_user, user_id), timeout=5.0
    )
    return {"id": user.id, "name": user.name}

Note asyncio.to_thread(fetch_user, user_id) — the function and its arguments passed separately, so nothing is called until the thread runs it. (If fetch_user were itself async def, the original line would have been correct as written. Which of those two worlds you are in is a fact about the file, and the prompt never said.)

Why this one is dangerous. A missing import is loud, immediate, and named after the thing that is missing — you fix it in ten seconds. The wait_for defect is behind it. Fix the import, watch the error message change, and it is very easy to read the second error as the tail of the first problem rather than a second one. Layered failures get diagnosed as a single failure, and the diagnosis stops at the first fix that changes the output.

The habit that catches it: run the code again after every fix, and keep running it until it produces the right answer — not until it produces a different error.

This is the value of working with Aider's format even outside Aider. The format makes failures visible at the point of application. It cannot make them visible at the point of execution — only running the code does that.

Next up: planning prompts for Claude Code-style agents. :::

Quiz

Module 5: IDE & Tool Prompts

Take Quiz
Was this lesson helpful?

Sign in to rate