Applying LLM-Generated Code Edits in October 2026: Search/Replace Blocks, Unified Diffs, and Whole-File Rewrites Without Corrupting Your Files

Getting a model to write a correct code change is only half the job. The other half is turning its output into an actual change on disk without mangling the file. Anyone who has built a coding assistant, a refactoring bot, or an agent that edits config files has seen the failure modes: a patch that applies to the wrong spot, a "rest of the file unchanged" comment that silently deletes 300 lines, or a diff whose line numbers are off by a few and fails to apply. This post walks through the three common edit formats, how to apply each one safely, and the checks that keep a bad edit from ever reaching your repository.

The vi text editor editing its own source code on a serial terminal
Image: Blastwave via Wikimedia Commons (CC BY-SA 4.0)

Why the edit format matters

A language model produces text, not file operations. Something in your code has to interpret that text as "change these bytes in this file." The format you ask for decides how much the model has to get exactly right, how many output tokens you pay for, and how easy it is to detect a mistake before you write anything. There is no single best format. The right choice depends on file size, how precise your model is at copying text, and whether you need edits to be reviewable by a human.

Format 1: Whole-file rewrites

The simplest approach: send the file, ask for the complete new version back, and overwrite the original.

When it works

  • Small files, roughly a few hundred lines or less, where output cost and latency are acceptable.
  • New files, where there is nothing to diff against.
  • Heavy rewrites that touch most of the file anyway.

How it breaks

  • Elision. Models sometimes write // ... rest of the code unchanged ... instead of the actual code. If you write that to disk, you delete everything it stood for.
  • Silent drift. The model "tidies up" code you did not ask it to touch: reordering imports, renaming a variable, dropping a comment.
  • Truncation. Long outputs can hit max_tokens and stop mid-file. Always check the finish reason; if it is a length stop, the output is incomplete and must be discarded.

Safeguards

  1. Reject output containing placeholder patterns such as ... existing code ..., rest of file, or unchanged inside a comment.
  2. Compare line counts. If the new file is dramatically shorter than the old one and the task was not "delete things," flag it.
  3. Compute a diff between old and new and show that, not the full file, to any human reviewer. Unrequested changes become obvious.

Format 2: Search/replace blocks

Here the model outputs pairs of "find this exact text" and "replace it with this text." Several open-source coding tools use variations of this idea, and some provider-hosted editor tools follow the same pattern with a str_replace-style command that requires the old text to match exactly once.

A typical block looks like this:

path/to/billing.py
<<<<<<< SEARCH
def total(items):
    return sum(i.price for i in items)
=======
def total(items, tax_rate=0.0):
    subtotal = sum(i.price for i in items)
    return round(subtotal * (1 + tax_rate), 2)
>>>>>>> REPLACE

Why it is popular

  • The model only emits the lines that change plus a little context, so output is much smaller than a full rewrite on large files.
  • No line numbers. Models are notoriously unreliable at counting lines, and this format avoids the problem entirely.
  • Failure is loud. If the search text is not in the file, you know the edit cannot be applied, instead of it landing somewhere wrong.

Applying it safely

  1. Require exactly one match. Zero matches means the model misremembered the code. More than one match means the edit is ambiguous. In both cases, refuse and return an error to the model that says which case occurred, so it can add more surrounding context.
  2. Apply blocks in order against the evolving file. If block two depends on text created by block one, applying both against the original will fail.
  3. Use a tolerant fallback carefully. A common fallback is to retry the match after normalizing trailing whitespace or line endings (\r\n versus \n). That is reasonable. Fuzzy matching on content is riskier: only accept it when there is a single clearly best candidate, and log every fuzzy apply so you can audit it.
  4. Preserve indentation. If you allow a match that ignores leading whitespace, re-indent the replacement to the indentation of the matched region. Python and YAML will break otherwise.
  5. Treat an empty search block as "create file." Only allow this when the file does not already exist, or you will clobber it.

Error messages that help the model recover

When a block fails, tell the model specifically what happened. "SEARCH text not found in billing.py; the closest region is lines 40 to 46, shown below" is far more useful than "edit failed." Include the actual current text of the nearest region so the next attempt copies from reality rather than memory. Cap the retries, for example two or three, then escalate to a whole-file rewrite or a human.

Format 3: Unified diffs and patch formats

The standard diff -u format is what developers already read in code review, which makes it attractive for human-in-the-loop flows:

--- a/billing.py
+++ b/billing.py
@@ -12,2 +12,3 @@
-def total(items):
-    return sum(i.price for i in items)
+def total(items, tax_rate=0.0):
+    subtotal = sum(i.price for i in items)
+    return round(subtotal * (1 + tax_rate), 2)

The line-number problem

Strict tools like git apply and patch expect hunk headers and context lines to be accurate. Models often get the @@ -12,2 +12,3 @@ numbers wrong. Two practical fixes:

  • Ignore the numbers and locate hunks by content. Treat the context and removed lines of each hunk as a search block, find the unique match, and apply. In effect you are converting the diff into search/replace.
  • Use a patch dialect without line numbers. Some agent tooling defines its own patch envelope with explicit add, update, and delete operations and context-anchored hunks rather than numeric offsets. If your provider documents such a format for its models, using it tends to work better than hand-rolled variants because the model has seen it often.

If you do run git apply, use git apply --check first to test without touching the working tree, and consider --3way so conflicts are surfaced as merge conflicts rather than a hard failure.

A single red jigsaw puzzle piece resting on top of green puzzle pieces
Image: Profpcde via Wikimedia Commons (CC0)

Choosing a format

  • Small file or new file: whole-file rewrite, with elision and truncation checks.
  • Large file, targeted change: search/replace blocks with an exact-single-match rule.
  • Human review is the main consumer: unified diff for display, but apply it by content matching rather than trusting line numbers.
  • Using a provider's built-in file-editing tool: use their documented commands as-is. Their models are typically tuned on that exact interface.

Whatever you pick, test it on your own model and codebase. Run a small set of real edit tasks through each candidate format and record the apply-success rate and the rate of incorrect-but-applied edits. That second number matters more, because a failed apply is visible and an applied wrong edit is not.

Validation after apply, before commit

Applying cleanly does not mean the edit is correct. Put a short pipeline between "patch applied" and "change kept":

  1. Work on a copy. Apply edits in a scratch branch, a temp directory, or an in-memory buffer. Never edit the only copy of a file in place.
  2. Parse it. Run the language's parser or a fast syntax check (python -m py_compile, node --check, tsc --noEmit, a JSON or YAML loader). This catches broken brackets and bad indentation in milliseconds.
  3. Format and lint. Running the project formatter normalizes whitespace noise the model introduced and makes the final diff smaller and easier to review.
  4. Run targeted tests. At minimum, the tests for the files you changed. Feed failures back to the model as a new turn, with the test output trimmed to the relevant part.
  5. Check the diff size. If the task was "fix a typo" and the diff touches 200 lines, stop and ask for review.
  6. Commit atomically. If a multi-file edit partially fails, roll back all of it. A half-applied refactor is worse than none.

Concurrency and stale context

Agents that run for a while hit a quieter problem: the file changed after the model read it. A teammate pushed, a formatter ran, or another agent step edited the same file. Defend against this by storing a hash of the file contents when you send it to the model, and refusing to apply if the current hash differs. Re-read the file and ask the model to regenerate the edit against the new version. Search/replace blocks degrade gracefully here, since an unrelated change elsewhere in the file does not stop the block from matching; line-numbered diffs do not.

Prompting tips that reduce bad edits

  • Show the model the current file contents right before asking for an edit, not something from twenty turns ago.
  • Give one short example of the exact format you will parse, including the file path line.
  • Tell it to include enough unchanged lines in each search block to make the match unique, and to never use placeholders.
  • Ask for one block per logical change rather than one giant block. Smaller blocks fail more precisely.
  • Keep your parser strict. Accepting "close enough" output trains you to tolerate drift that will eventually bite.

A minimal apply function

Here is the core of an exact-match applier in Python. It is deliberately small; add whitespace normalization and logging around it as needed.

class EditError(Exception):
    pass

def apply_block(text: str, search: str, replace: str) -> str:
    if search == "":
        raise EditError("empty SEARCH only allowed when creating a new file")
    count = text.count(search)
    if count == 0:
        raise EditError("SEARCH text not found")
    if count > 1:
        raise EditError(f"SEARCH text matches {count} places; add more context")
    return text.replace(search, replace, 1)

def apply_all(text: str, blocks: list[tuple[str, str]]) -> str:
    for search, replace in blocks:
        text = apply_block(text, search, replace)  # apply against evolving text
    return text

Wrap apply_all so that any EditError leaves the original file untouched, then return the error message to the model as the tool result.

The takeaway

Most "the AI broke my file" stories are not model failures. They come from an apply step that trusted the output too much. Pick a format that fits the file size and your review flow, insist on exact and unique matches, never write elided or truncated output, check that the file is unchanged since the model read it, and run a parse-lint-test pass before anything is committed. Those few rules turn code-editing agents from a liability into something you can leave running.

Comments

Popular posts from this blog

Grok Bot - a step closer to AGI

Structured Outputs and Constrained Decoding for Reliable LLM APIs in Production (September 2026)

Tencent Hy4 Preview: Open 770B MoE Built for Real Work