Exact-Match Edits vs Patches: How Claude Code, Codex and Aider Write to Your Files
Book: Prompt Engineering Pocket Guide Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go My project: Hermes IDE | GitHub — an IDE for developers who ship with
- Book: Prompt Engineering Pocket Guide
- Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
Your config file has two functions that both set timeout = 30. You ask a coding agent to raise the production timeout to 60. Depending on which agent you asked, one of two things happens. The edit is refused until the agent adds more context. Or the first timeout = 30 in the file changes, which is the development one, and the patch applies cleanly.
The edit protocol decides that outcome: the format the agent uses to describe a change, and the code on your machine that applies it. Claude Code, Codex and Aider each use a different one, and their docs and source say exactly what happens when the text doesn't line up.
Here is the file used for every case below:
def load_dev():
timeout = 30
retries = 3
def load_prod():
timeout = 30
retries = 5
Three ways to say "change this line"
Claude Code: exact string replacement. The Edit tool takes an old_string and a new_string and, in the docs' words, "doesn't use regex or fuzzy matching." Three checks must pass: Claude has read the file, old_string appears exactly as written, and it appears exactly once unless replace_all: true is set. The read rule is looser for newer models. The same page says Claude Opus 4.6, Claude Haiku 4.5 and older models always require the read, while newer models on Claude Code v2.1.208 or later can skip it when the Read tool is available and reading wouldn't need a permission prompt.
The documented fields, for our task:
Edit
old_string: " timeout = 30"
new_string: " timeout = 60"
replace_all: false
Codex: patch envelopes. OpenAI's Codex Prompting Guide tells you to use apply_patch for single-file edits, says the model "has been trained to excel at this diff format", and strongly recommends using OpenAI's exact implementation. That implementation is open source in the codex-rs/apply-patch crate. Its markers are *** Begin Patch, *** Add File:, *** Delete File:, *** Update File:, *** Move to:, *** End of File, @@ and *** End Patch. Changed lines start with - or +, context lines with a space, and there are no line numbers:
*** Begin Patch
*** Update File: config.py
@@
- timeout = 30
+ timeout = 60
*** End Patch
Aider: you pick the format. Aider's edit formats include whole (the model returns the entire file), diff (SEARCH/REPLACE blocks), diff-fenced (the same blocks with the path inside the fence, mainly for Gemini models), udiff (a simplified unified diff) and two editor- variants for architect mode. A diff block looks like a merge conflict:
config.py
```python
<<<<<<< SEARCH
timeout = 30
=======
timeout = 60
>>>>>>> REPLACE
```
Aider also publishes how often models get the format right. Its polyglot leaderboard, last updated November 20, 2025, puts a "Correct edit format" column next to the pass rate over 225 exercises. gpt-5 (high) tops the table at 88.0% correct, with 91.6% on edit format using diff. Even the top model sent a malformed edit in some cases, and the applier has to catch each one.
The duplicate line: one refuses, two pick the first
Claude Code's uniqueness check fails, because timeout = 30 appears twice. Per the docs, Claude then "either supplies a longer string with enough surrounding context to pin down one occurrence, or sets replace_all: true to replace them all." The retry that works carries the function header:
Edit
old_string: "def load_prod():\n timeout = 30"
new_string: "def load_prod():\n timeout = 60"
replace_all: false
Codex's applier doesn't check uniqueness. In file_update.rs, each chunk's old lines are searched from the current position forward, and seek_sequence.rs returns the first window that matches without looking at later ones. With the bare @@ patch above, the first timeout = 30 sits in load_dev. That line changes.
The anchor fixes it. Text after @@ is a change context: the applier finds that line first and only searches for the old lines after it.
*** Begin Patch
*** Update File: config.py
@@ def load_prod():
- timeout = 30
+ timeout = 60
*** End Patch
Aider's diff format behaves like Codex here. In editblock_coder.py, perfect_replace replaces the first block of lines that matches and returns. One path is stricter. When a SEARCH block uses ... to elide lines, Aider refuses the block if that chunk appears more than once.
To be fair to Codex and Aider, models rarely send one bare line. Context lines in a patch and a multi-line SEARCH block usually make the match unique anyway. The difference shows up when they don't: Claude Code stops, the other two apply at the first match.
Stale context: the file moved since the model looked
The model's copy of a file goes stale when you edit it, a formatter runs, or a second agent touches it.
Claude Code has a documented rule for this case. Since v2.1.208, if the file changed after Claude's last read but old_string still matches the current content "exactly and unambiguously" and the file is readable without a prompt, the edit applies and the result notes that the file carries other changes, so Claude re-reads before edits that depend on the surrounding code. In any other case, such as a stale old_string, Claude reads the file again before editing. Before v2.1.208 it refused any edit to a file that changed on disk after the read.
Codex fails with a message the model can act on. If the @@ line isn't found, file_update.rs returns Failed to find context '<line>' in <path>. If the old lines aren't found, it returns Failed to find expected lines in <path>: followed by the lines it looked for. The API guide describes the same loop for your own harness: report status: "failed" with a short human-readable message.
Aider replies with SearchReplaceNoExactMatch: This SEARCH block failed to exactly match lines in <path>, plus the reminder that "The SEARCH section must exactly match an existing block of lines including all white space, comments, indentation, docstrings, etc". If misses keep coming, its troubleshooting page suggests, among other things, --edit-format whole.
The whole format can't miss a match, because there is nothing to match. If the model's copy of the file was old, though, the rewrite puts that old version back over whatever changed since, and nothing reports an error. The other risk is elision. The edit-formats page says udiff was mainly used with GPT-4 Turbo because it reduced that model's habit of eliding code with comments like "original code here". In Aider's unified diffs write-up, moving GPT-4 Turbo from SEARCH/REPLACE to udiff raised its laziness benchmark score from 20% to 61%.
Whitespace drift: strict, forgiving, and in between
Models re-type the text they want to replace, and re-typing drifts. A tab comes back as four spaces, trailing whitespace disappears, a straight quote turns curly.
Claude Code is strict on purpose. The docs say "A single character of whitespace or indentation difference is enough to miss." When it misses, the edit doesn't apply.
Codex's applier is the most forgiving of the three. seek_sequence makes four passes in what its comment calls "decreasing strictness": an exact match, then ignoring trailing whitespace, then trimming both sides, then normalising common Unicode punctuation like dashes and quotes to ASCII. Indentation drift in the old lines still finds its target. The price shows up when nothing matches exactly. A looser pass can match in more places, and the first window it finds wins.
Aider sits in between. It tries an exact match, then a match that tolerates missing leading whitespace, then retries without a stray blank first line. A fuzzy edit-distance matcher exists in the same file, but a bare return sits in front of the call, so it never runs.
Leniency pays off. The same unified diffs write-up reports that turning flexible patching off produced "a 9X increase in editing errors".
| Failure | Claude Code Edit | Codex apply_patch | Aider diff |
|---|---|---|---|
| Duplicate match | Refused | First match after anchor | First match |
| Stale text | Applies if still exact, else re-reads | Error to model | Error to model |
| Whitespace drift | Miss | Four lenient passes | Tolerates leading whitespace |
An undo that doesn't care how the bytes got there
Every failure above has a quiet version: the edit applies in the wrong place, or on top of stale assumptions, and the agent reports success. You catch it in review, and then you want that one turn back.
Each tool has its own answer. Claude Code's checkpoints capture your code before each prompt, and /rewind restores code, conversation or both. The docs are clear about the edge: "Checkpointing does not track files modified by Bash commands", and most subagent edits aren't restored either. Aider commits every edit to git with a descriptive message, and /undo discards the last change. Each one covers its own agent's edits and nothing else.
I build Hermes IDE, so weigh this with that in mind. Hermes runs Claude Code, Codex, Aider and other agents, each in its own terminal, and saves every agent turn, so you can diff or restore any single turn, with undo (README). The strip under a session lists the agent's replies with the lines each changed. Click one to see its changes or restore your files to that point (2.3.1 notes). The Review Desk (⌘G) shows the same work by turn or by file.

The Review Desk (⌘G) in By turn view for a Claude Code task: two turns, turn 1's diff of src/db.ts, the changed files, and a commit message drafted from the turns.
Because the snapshot is of your files, not of the edit calls, the same restore works for every agent, whether the change arrived as an old_string, a patch envelope or a SEARCH block. There are two limits. In a Claude Code task with extra folders, the strip tracks the main project only (2.3.1 notes). Turn snapshots from tasks that ran in their own worktree are removed 14 days after the session closes (2.2.9 notes).
What to do with this
If you run Codex or Aider on files with repeated blocks, like per-environment config or test tables, check where the diff landed, not only what it says.
If Claude Code keeps missing an edit, look for mixed tabs and spaces or invisible characters in the file before you blame the model. If you use Aider's whole format, keep other writers off that file while the agent works, because a rewrite from a stale copy applies without an error.
Whatever the protocol, keep a per-turn record outside the edit path: plain commits, Aider's auto-commit, or a turn snapshot.
If this was useful
An edit format is structured output with a strict parser on the other end, and Aider's leaderboard shows that even top models miss the format in some share of cases. If you write prompts that ask a model for output a program has to parse, the Prompt Engineering Pocket Guide is my short book on prompting techniques, structured output among them. If you run several of these agents side by side, Hermes IDE is free to download, source-available under BSL 1.1, runs on macOS, Windows and Linux, and lives on GitHub.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.
