OpenAI Codex · Code Mode

apply-patch: A Diff Designed for Models

The model fills in a line-number-free patch; people see a unified diff computed after the fact. Same edit — watch where each format wrecks.

Course goalAfter reading, you can explain two things: why the model’s diff writes only one context-anchor line, not the four numbers in @@ -l,s +l,s; and why a single file stays off disk when context misses — and why that promise does not hold across files.
Try it first · Same edit, two writings
Replace pass in greet with return 123
File on disk
Switch to two inserted lines — the four numbers on the left drift. Switch to a missing anchor — the right side stops writing.
unified diffFour numbersmust be right
Waiting to start.
apply-patchOne-line anchorsearch now
@@ def greet():
Waiting to start.
Logic trail · which source span each animation step maps to
  1. The grammar’s @@ is an anchor only — no start line, no spanparser.rs L20
  2. Update-file chunks must stay in the order they appear in the fileparser.rs L74
  3. With change_context, search downward from line_indexfile_update.rs L99
  4. Exact whole-line first, then strip trailing whitespace, then trim both sidesseek_sequence.rs L40
  5. If the anchor is missing, fail immediately with Failed to find context — no nearby-line guessingfile_update.rs L109
  6. All chunks of one file finish in memory before write_file runslib.rs L695
  7. A cross-file failure returns with already-committed deltas — no rollbacklib.rs L453
  8. The unified diff people see is computed later with TextDifffile_update.rs L328
Hit Play to see how the same edit locates under two diff writings.
Four numbersA unified-diff @@ header must get old start, old span, new start, and new span all right. Insert two lines above, and all four numbers die together.
One-line anchorapply-patch writes only @@ def greet(): and searches at runtime. Two inserted lines still match — line numbers never entered this format.
Stop when it missesWhen the anchor line is rewritten, the right side reports Failed to find context and the file stays as-is. The left-hand format fuzzes near the line number and may land on a neighboring function.
Teaching sketch: file hunks and line numbers are course settings, used to contrast how the two formats locate. Line numbers on the logic trail match openai/codex commit 4f39251a01.
Idea 1 · The model fills one format; people read another
What problem it solves

You ask the model to change a function: replace pass in greet with return 123. It emits a standard unified diff whose first line reads @@ -47,3 +47,3 @@.

Another edit just inserted two lines, so greet is already on line 49. The model counted inside a numbered excerpt, then has to add start and span itself when writing the patch. Getting all four numbers wrong is the normal case.

patch(1) hunts by line number, then fuzzes if it misses. Fail the fuzz and the whole patch dies. Worse: it may stick the change onto a neighboring function. Tests stay green — you just edited the wrong function.

What the idea is

Codex splits filling from reading. The copy the model fills has no line numbers. Updating a hunk, the first line is just @@ def greet():. A bare @@ means keep searching from here. Location is handed to runtime seek_sequence.

When a person reads the change, the UI separately builds a standard unified diff with similar::TextDiff. The Codex format in the tool arguments is done by then.

Source:codex-rs/apply-patch/src/file_update.rs lines 328–329

The grammar writes “no line numbers” into the productions. Both @@ forms carry a text anchor only:

codex-rs/apply-patch/src/parser.rslines 20–22
//! change_context: ("@@" | "@@ " /(.+)/) LF
//! change_line: ("+" | "-" | " ") /(.+)/ LF
//! eof_line: "*** End of File" LF
Source snapshot note: based on the local openai/codex repo; verified against codex-rs/apply-patch/src/parser.rs, commit 4f39251a01, verified 2026-08-22. Code blocks keep the original source. These three lines are the diff header for the model: anchors, no line numbers.

The spec sent to the model calls this language a “stripped-down, file-oriented diff format designed to be easy to parse and safe to apply.” Add, Delete, and Move are three markers in the grammar; the parser dispatches on the marker. The model never has to remember how to assemble ---, +++, /dev/null, or a rename header.

Model fills Model Writes a patch apply-patch @@ anchor, no line numbers seek_sequence Search disk now, compute new text People read Old and new text already computed Tool-arg format is finished here TextDiff Build unified diff after the fact UI The copy people see
Teaching diagram: same edit — the model fills anchors, people see line numbers.
Why it lasts

Coordinates come from addition; content comes from recognition. Having the model count line numbers doesn’t get better if you swap the model or the language. Moving “which hunk” from fill-time arithmetic to apply-time string search doesn’t depend on Rust, or on unified diff as a format.

Idea 2 · Whitespace can loosen; position is never guessed
What problem it solves

When the model writes a patch, a trailing space, or an en-dash in the file that it wrote as a minus, are high-frequency accidents. Fail the whole patch every time and the model can only rewrite. Retry a few lines near the number, and you’re back to fuzz landing on the wrong function.

What the idea is

seek_sequence searches four grades, tight to loose. Grade 1: exact whole-line equality. Grade 2: drop trailing whitespace, then compare. Grade 3: trim() both sides. Grade 4: fold common Unicode dashes and curly quotes to ASCII. Fail all four and it returns empty — “Failed to find context” or “Failed to find expected lines.” There is no nearby-line retry loop.

Source:codex-rs/apply-patch/src/seek_sequence.rs lines 40–114

An early incident added the fourth grade specifically for odd Unicode. It loosens whitespace and punctuation, not position. Full-width Chinese quotes are not in the normalize table: if the model writes a full-width left quote and the file has a half-width one, all four grades fail.

Exact equal trim_end Trim both sides normalise All four fail, return empty No sliding a few lines by number Fail at once
Teaching flowchart: spaces and dashes can pass; a line-number shift is not in these four grades.
Why it lasts

Tolerance has to split two kinds of difference. Trailing spaces and curly quotes are meaningless byte noise — normalize them. A two-line number shift is the wrong place — error out and make the model rewrite. That boundary still holds if you rewrite in another language.

Idea 3 · Finish one file in memory, then write; no cross-file transaction
What problem it solves

A patch has two chunks. The second misses, the first already went in, and the file becomes a half-finished product. The person debugging sees a half-applied file — harder to fix than a total failure.

What the idea is

Inside one file, compute_replacements first collects each chunk into a replacement list. One miss returns an error immediately — write_file never runs. That file stays as-is.

Source:codex-rs/apply-patch/src/file_update.rs lines 109–113

Across files it’s another story. apply_hunks_to_files writes hunks in order; on failure it returns with already-committed AppliedPatchDelta — the loop has no rollback. Test 015 nails this: it first succeeds at adding created.txt, then updates a path that does not exist, and created.txt is still on disk.

Source: codex-rs/apply-patch/src/lib.rs line 453, and the hunk loop from line 504
Two chunks in the same file chunk 1 finished in memory chunk 2 misses, return write_file never ran; file as-is Two file-level hunks Add File already on disk Update a path that doesn’t exist delta stays; created.txt remains
Teaching contrast: “no partial success” holds only for a single file.
The model fills anchors. People see line numbers. One file misses — don’t write.
Why it lasts

The scope you compute-then-commit must match the unit you can handle atomically. One file can finish every replacement in memory, then write once. Multiple files already hit disk; rollback means writing again, plus half-success cases like Move where both source and target moved. Cross-file transactions are a product choice, not a promise of the format. In the spec you give the model, don’t sell the single-file guarantee as a global one.

Side-by-side · Another answer to the same question

DeepSeek Harness: read first, then edit

DSH’s editIntent checks whether this session has observed the file. No observation, and it throws FS_NOT_OBSERVED. What it records is a version joined from dev:ino:size:mtimeNs:ctimeNs, not a content hash.

Even past that gate, applyLiteralEdit by default requires old_string to appear once; multiple hits throw FS_AMBIGUOUS_EDIT. The safety sits on an event gate and literal uniqueness. Codex has no read-first constraint: location lives in the patch and is searched at runtime; if old_lines appears twice it takes the first, with no ambiguity error.

Both sides verified against source · 2026-08-22 · DSH · The engineering of file edits

Claude Code: refuse if unread; refuse if it hits more than once

FileEditTool wants two things at once. The file must have been read; unread reports errorCode 6, verbatim “File has not been read yet.” If old_string appears more than once and replace_all is false, it reports errorCode 9 and asks for more context so this occurrence is singled out.

Fuzz covers quotes only. findActualString searches exact first, then folds curly quotes to straight ones. No Codex-style three trailing-whitespace grades, and no dash normalization. Want one fewer tool round trip? Copy Codex’s format. Want more specific error text for the model? Copy these refusals with errorCodes.

Both sides verified against source · 2026-08-22
Classroom Exercise
01

Two identical old_lines — which one gets edited?

The file has two identical old_lines blocks. The model only wants the second, but didn’t give enough @@ anchors. Which block does seek_sequence edit, and why?

Follow-up: if you want only the second block, which line should the anchor sit in front of? In the same patch, if you first succeed at adding a file, then update a path that doesn’t exist, what stays on disk?

Takeaway:Don’t put line numbers in the model’s diff; let runtime search context. Whitespace and punctuation can loosen grade by grade; position is never guessed. One file misses — don’t write. Across files, what already landed stays on disk.