Slots a new "apply_patch" variant alongside the existing edit modes
(replace, patch, hashline, chunk, vim). The mode accepts a single input
string containing a Codex *** Begin Patch / *** End Patch envelope,
parses it with a new lenient parser (heredoc-tolerant), and fans each
file-op out to the existing executePatchSingle so LSP writethrough,
plan-mode guards, fs-cache invalidation and diagnostics are shared
with the patch mode.
Exposes both tool shapes from the spec: the JSON function-tool variant
(§1.2, {input: string}) and the OpenAI custom-tool / Lark-grammar
"freeform" variant (§1.1, raw patch string). The edit tool advertises
a Lark grammar via customFormat and a wire name via customWireName;
openai-responses emits it as a grammar-constrained custom tool when a
model opts in with applyPatchToolType: "freeform" in models.json.
custom_tool_call / custom_tool_call_output are plumbed end-to-end
through the shared responses code (emission, streaming, history
replay), and the agent-loop dispatcher matches tool calls by either
name or customWireName so returned calls route correctly.
Also threads preview/diff rendering for apply_patch through the TUI
(tool-execution + edit renderer) so streaming patches show per-file
diffs like the other edit modes.
Default edit mode is unchanged (hashline); opt in via edit.mode or
PI_EDIT_VARIANT=apply_patch.
33 KiB
apply_patch: Codex Patch Format Specification
This document is a full, reimplementation-quality specification of the apply_patch
tool used by the Codex coding harness. It covers:
- How the tool is exposed to the model (schema + freeform grammar variants).
- The exact prompt / instruction text shown to the model.
- The patch format grammar.
- The parser (lexical rules, lenient mode, streaming mode, errors).
- The application algorithm (Add / Delete / Update / Move), including the
seek_sequencefuzzy matcher. - Invocation forms the harness accepts (freeform args, JSON args, shell heredoc wrappers, stdin).
- Result presentation and error reporting.
- Test-derived edge cases.
All normative behavior below is drawn from the codex-rs/apply-patch crate
(the parser in src/parser.rs, the applier in src/lib.rs, the matcher in
src/seek_sequence.rs) and the tool registration in codex-rs/tools.
1. Tool registration
apply_patch is registered in the tool registry when the current model's
apply_patch_tool_type metadata is set (see
codex-rs/models-manager/models.json — GPT-5.x variants use "freeform";
older / non-reasoning models use "function"). It is registered as
supports_parallel_tool_calls = false, meaning the harness will not issue
two concurrent apply_patch calls. (Registration in
codex-rs/tools/src/tool_registry_plan.rs.)
There are two wire formats for the same underlying command.
1.1 Freeform (GPT-5 and later)
The freeform variant uses OpenAI's custom-tool mechanism: the model emits a single opaque string whose shape is constrained by a Lark grammar.
{
"type": "custom",
"name": "apply_patch",
"description": "Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.",
"format": {
"type": "grammar",
"syntax": "lark",
"definition": "<see §3.2>"
}
}
(See codex-rs/tools/src/apply_patch_tool.rs::create_apply_patch_freeform_tool.)
The freeform call payload is the patch text itself — no JSON envelope, no wrapping quotes. Example (the model types this verbatim as the tool's "input"):
*** Begin Patch
*** Add File: hello.txt
+Hello, world!
*** End Patch
1.2 JSON function (legacy / gpt-oss)
For providers that only support function-style tool calls, a JSON variant is registered:
{
"type": "function",
"name": "apply_patch",
"description": "<APPLY_PATCH_JSON_TOOL_DESCRIPTION, see §2>",
"strict": false,
"parameters": {
"type": "object",
"additionalProperties": false,
"required": ["input"],
"properties": {
"input": {
"type": "string",
"description": "The entire contents of the apply_patch command"
}
}
}
}
(See codex-rs/tools/src/apply_patch_tool.rs::create_apply_patch_json_tool.)
1.3 Handler dispatch
Both variants land in the same handler, which normalizes the arguments into
ApplyPatchToolArgs { input: String } and passes input to
apply_patch::apply_patch(...) (see
codex-rs/core/src/tools/handlers/apply_patch.rs). The input is the full
patch text, including the *** Begin Patch / *** End Patch envelope.
2. Agent prompt (verbatim)
The model is taught the format via two equivalent pieces of text: the
Markdown file codex-rs/apply-patch/apply_patch_tool_instructions.md
(embedded in system prompts), and the string constant
APPLY_PATCH_JSON_TOOL_DESCRIPTION in
codex-rs/tools/src/apply_patch_tool.rs which is used as the JSON tool's
description. They are identical in content.
A reimplementation SHOULD ship the following text verbatim (note: the original uses Unicode curly quotes in a few places — they are reproduced here; the final "smart quote" versus "typewriter quote" choice is not semantic):
## `apply_patch`
Use the `apply_patch` shell command to edit files.
Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope:
*** Begin Patch
[ one or more file sections ]
*** End Patch
Within that envelope, you get a sequence of file operations.
You MUST include a header to specify the action you are taking.
Each operation starts with one of three headers:
*** Add File: <path> - create a new file. Every following line is a + line (the initial contents).
*** Delete File: <path> - remove an existing file. Nothing follows.
*** Update File: <path> - patch an existing file in place (optionally with a rename).
May be immediately followed by *** Move to: <new path> if you want to rename the file.
Then one or more "hunks", each introduced by @@ (optionally followed by a hunk header).
Within a hunk each line starts with:
For instructions on [context_before] and [context_after]:
- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change's [context_after] lines in the second change's [context_before] lines.
- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
@@ class BaseClass
[3 lines of pre-context]
- [old_code]
+ [new_code]
[3 lines of post-context]
- If a code block is repeated so many times in a class or function such that even a single `@@` statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple `@@` statements to jump to the right context. For instance:
@@ class BaseClass
@@ def method():
[3 lines of pre-context]
- [old_code]
+ [new_code]
[3 lines of post-context]
The full grammar definition is below:
Patch := Begin { FileOp } End
Begin := "*** Begin Patch" NEWLINE
End := "*** End Patch" NEWLINE
FileOp := AddFile | DeleteFile | UpdateFile
AddFile := "*** Add File: " path NEWLINE { "+" line NEWLINE }
DeleteFile := "*** Delete File: " path NEWLINE
UpdateFile := "*** Update File: " path NEWLINE [ MoveTo ] { Hunk }
MoveTo := "*** Move to: " newPath NEWLINE
Hunk := "@@" [ header ] NEWLINE { HunkLine } [ "*** End of File" NEWLINE ]
HunkLine := (" " | "-" | "+") text NEWLINE
A full patch can combine several operations:
*** Begin Patch
*** Add File: hello.txt
+Hello world
*** Update File: src/app.py
*** Move to: src/main.py
@@ def greet():
-print("Hi")
+print("Hello, world!")
*** Delete File: obsolete.txt
*** End Patch
It is important to remember:
- You must include a header with your intended action (Add/Delete/Update)
- You must prefix new lines with `+` even when creating a new file
- File references can only be relative, NEVER ABSOLUTE.
You can invoke apply_patch like:
```
shell {"command":["apply_patch","*** Begin Patch\n*** Add File: hello.txt\n+Hello, world!\n*** End Patch\n"]}
```
Notes for implementers:
- The "You can invoke apply_patch like ..." footer is only shown when the
patch command is exposed as a
shellcommand (thecodexfallback path). When the freeform tool is registered, the model invokes the tool directly and the footer is redundant but harmless. - The prompt asserts some things the parser is lenient about in practice:
- "File references can only be relative" — the parser accepts absolute paths, but the model is instructed to produce relative ones.
- "+ line even when creating a new file" —
+MUST be the first character of every content line in an Add File section.
3. Patch grammar
3.1 EBNF (canonical)
Patch := Begin { FileOp }+ End
Begin := "*** Begin Patch" NEWLINE
End := "*** End Patch" [NEWLINE]
FileOp := AddFile | DeleteFile | UpdateFile
AddFile := "*** Add File: " path NEWLINE { "+" text NEWLINE }+
DeleteFile := "*** Delete File: " path NEWLINE
UpdateFile := "*** Update File: " path NEWLINE
[ "*** Move to: " path NEWLINE ]
Change?
Change := (ChangeContext | ChangeLine)+ EofLine?
ChangeContext := ("@@" | "@@ " text) NEWLINE
ChangeLine := (" " | "-" | "+") text NEWLINE
EofLine := "*** End of File" NEWLINE
The Lark grammar that the OpenAI freeform tool uses to constrain model
output is in codex-rs/tools/src/tool_apply_patch.lark:
start: begin_patch hunk+ end_patch
begin_patch: "*** Begin Patch" LF
end_patch: "*** End Patch" LF?
hunk: add_hunk | delete_hunk | update_hunk
add_hunk: "*** Add File: " filename LF add_line+
delete_hunk: "*** Delete File: " filename LF
update_hunk: "*** Update File: " filename LF change_move? change?
filename: /(.+)/
add_line: "+" /(.*)/ LF -> line
change_move: "*** Move to: " filename LF
change: (change_context | change_line)+ eof_line?
change_context: ("@@" | "@@ " /(.+)/) LF
change_line: ("+" | "-" | " ") /(.*)/ LF
eof_line: "*** End of File" LF
%import common.LF
3.2 Reserved tokens
| Token | Meaning |
|---|---|
*** Begin Patch |
Required first significant line of the envelope. |
*** End Patch |
Required last significant line (trailing LF optional). |
*** Add File: <path> |
Start of an Add File section. |
*** Delete File: <path> |
Standalone Delete File directive. |
*** Update File: <path> |
Start of an Update File section. |
*** Move to: <path> |
Optional rename target, immediately after *** Update File:. |
@@ or @@ <header> |
Starts a chunk inside an Update File. |
*** End of File |
Terminates a chunk; asserts the chunk ended at EOF. |
+<text> / -<text> / <text> |
Added / deleted / context line inside a chunk. |
Action headers match with a trailing space: the parser uses literal
strip_prefix("*** Add File: ") etc. Everything after the space is the
path; no escaping, no quoting.
3.3 Lines and newlines
- Input is split on
\n(LF only). CRLF is not supported by the parser — producers MUST use LF. (Tool output goes through Rust string handling which preserves CRs as literal bytes on the content lines, which then fail to match.) - Each content line in a hunk starts with exactly one byte (
+,-, or space) followed by the line's text and then a newline. An empty-prefixed line is representable as a single space followed by LF; a bare+\nis a one-character added empty line. - A completely blank line (no prefix byte at all) inside an Update File section is skipped, and is used only for visual separation between chunks. This is a deliberate leniency; do not rely on blank lines to carry data.
4. Parser
Implementation: codex-rs/apply-patch/src/parser.rs.
4.1 Public entry points
parse_patch(text: &str) -> Result<ApplyPatchArgs, ParseError>— production parse. UsesParseMode::Lenient(see §4.3).parse_patch_streaming(text: &str) -> Result<ApplyPatchArgs, ParseError>— same format, but tolerates a missing*** End Patch(the harness calls this from a streaming response handler to show progress). Its output MUST NOT be used to actually apply the patch.
The result is:
pub struct ApplyPatchArgs {
pub patch: String, // canonicalized patch text (heredoc stripped)
pub hunks: Vec<Hunk>,
pub workdir: Option<String>, // populated only when parsing a shell
// invocation that begins with `cd <path> &&`
}
4.2 Lexical canonicalization
Before parsing, the input is trim()ed, then split into lines by \n.
Marker-line matching is done against line.trim(), so arbitrary
leading/trailing whitespace around the sentinel lines (*** Begin Patch,
etc.) is accepted. Content lines (those starting with +, -, ) are
NOT trimmed — their whitespace is significant.
4.3 Parse modes
The parser operates in one of three modes:
-
Strict — requires line 0 =
*** Begin Patchand the last line to be*** End Patch. Not used by the harness today (PARSE_IN_STRICT_MODE = false); retained as a fallback. -
Lenient (default). Tries strict first. If that fails, attempts to strip a heredoc wrapper: the first line must be exactly one of
<<EOF,<<'EOF', or<<"EOF", and the last line must beEOF. The inner region is then parsed strictly. This was introduced to handle gpt-4.1, which insisted on wrapping the patch in a heredoc body. Mismatched quotes (e.g.<<"EOF') are rejected. -
Streaming — requires
*** Begin Patchbut does NOT require*** End Patch. Individual hunks are parsed on a best-effort basis; the last incomplete hunk is dropped. Used for progress UI only.
4.4 Hunk parsing state machine
loop:
trim line[i]
if line[i] starts with "*** End Patch": break
match line[i].strip_prefix(...):
"*** Add File: " -> parse AddFile
"*** Delete File: " -> parse DeleteFile
"*** Update File: " -> parse UpdateFile
otherwise -> InvalidHunkError at line i
AddFile consumes subsequent lines while they start with +. Each line
becomes one element of contents, with the leading + stripped. The lines
are joined with \n; an additional \n is appended after the join so the
resulting file ends with exactly one newline.
DeleteFile consumes only the header line; no content follows.
UpdateFile consumes the header, optionally a *** Move to: <path> line,
then zero or more chunks. The UpdateFile section ends when the next line
starts with *** (the next hunk header, or *** End Patch) or input is
exhausted. An UpdateFile section with zero chunks is an error
("Update file hunk for path '<p>' is empty").
Chunk parsing within an UpdateFile:
chunk:
optional context line:
"@@" -> change_context = None (empty marker)
"@@ <header>" -> change_context = Some("<header>")
otherwise -> if this is the FIRST chunk of the hunk, fall through
(context-less first chunk); otherwise error.
then:
loop over lines:
if line starts with "*" -> stop (end of chunk; next hunk or end)
if line == "" -> append empty string to BOTH old_lines
and new_lines (empty context line)
if line starts with ' ' -> append line[1..] to BOTH old_lines and
new_lines
if line starts with '-' -> append line[1..] to old_lines
if line starts with '+' -> append line[1..] to new_lines
if line == "*** End of File" -> set is_end_of_file = true, stop
otherwise -> error: unexpected line in update hunk
Invariants enforced by the parser:
- A chunk must contain at least one non-context line (pure context chunks are rejected).
*** End of Filecannot be the first line of a chunk.- Chunks are stored in order and the applier relies on each chunk's match position being ≥ the previous chunk's match position.
4.5 Error taxonomy
pub enum ParseError {
InvalidPatchError(String), // envelope errors
InvalidHunkError { message: String, line_number: usize }, // content errors
}
Specific messages (implementers SHOULD match these literally so tests and downstream tools keep working):
| Error | Condition |
|---|---|
The first line of the patch must be '*** Begin Patch' |
Missing/wrong Begin marker. |
The last line of the patch must be '*** End Patch' |
Missing/wrong End marker (strict/lenient only). |
'<line>' is not a valid hunk header. Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}' |
Unknown file-level directive. |
Update file hunk for path '<p>' is empty |
UpdateFile section has zero chunks. |
Expected update hunk to start with a @@ context marker, got: '<line>' |
Missing @@ when required (non-first chunk). |
Update hunk does not contain any lines |
Chunk with a context but no +/-/space line before EOF/next chunk. |
Unexpected line found in update hunk: '<line>'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line) |
Invalid diff prefix. |
5. Intermediate representation
pub enum Hunk {
AddFile { path: PathBuf, contents: String },
DeleteFile { path: PathBuf },
UpdateFile {
path: PathBuf,
move_path: Option<PathBuf>,
chunks: Vec<UpdateFileChunk>, // MUST be non-empty
},
}
pub struct UpdateFileChunk {
pub change_context: Option<String>, // text after "@@ "; None for bare "@@"
pub old_lines: Vec<String>, // lines to match in the file
pub new_lines: Vec<String>, // replacement lines
pub is_end_of_file: bool, // "*** End of File" present
}
Paths are stored exactly as the patch wrote them — no canonicalization.
Resolution to an absolute path happens at apply time via
AbsolutePathBuf::resolve_path_against_base(path, cwd).
6. Application algorithm
Implementation: codex-rs/apply-patch/src/lib.rs.
6.1 Top-level flow
apply_patch(input, cwd, fs, sandbox):
args := parse_patch(input)?
if args.hunks is empty: error "No files were modified."
affected := { added: [], modified: [], deleted: [] }
for each hunk in args.hunks:
apply_hunk(hunk, cwd, fs, sandbox, affected)?
return affected
Hunks are applied in the order they appear in the patch and not
atomically: if hunk N fails, hunks 0..N-1 have already written to disk
and are not rolled back. Hunks N+1.. are skipped. (Test scenario
015_failure_after_partial_success_leaves_changes pins this behavior.)
A reimplementation MAY add transactional semantics, but MUST document the deviation — callers today rely on partial application being observable.
6.2 Add File
path_abs := resolve_path_against_base(hunk.path, cwd)
try: fs.write_file(path_abs, contents)
on NotFound: fs.create_directory(parent(path_abs), { recursive: true })
fs.write_file(path_abs, contents)
append hunk.path to affected.added
Characteristics:
- Parent directories are created on demand (recursive). This is done lazily
— only after a first write fails with
NotFound. - An existing file at
path_absis silently overwritten (scenario011_add_overwrites_existing_file). No confirmation, no diff. contentsis the literal joined string from the parser — the parser already terminates it with\n.
6.3 Delete File
path_abs := resolve_path_against_base(hunk.path, cwd)
meta := fs.get_metadata(path_abs)
if meta.is_directory: error "path is a directory"
fs.remove(path_abs, { recursive: false, force: false })
append hunk.path to affected.deleted
- If the file does not exist, the metadata call returns
NotFoundand the wholeapply_patchfails withFailed to delete file <p>: .... - Deleting directories is explicitly rejected.
6.4 Update File
applied := derive_new_contents_from_chunks(path_abs, hunk.chunks, fs)?
if hunk.move_path.is_some():
dest_abs := resolve_path_against_base(move_path, cwd)
write_with_missing_parent_retry(dest_abs, applied.new_contents)
ensure source isn't a directory; fs.remove(path_abs)
else:
fs.write_file(path_abs, applied.new_contents)
append hunk.path to affected.modified
Note: a renamed file is reported as modified (M) with the original
path, not as D + A. This is intentional — it mirrors git's rename
detection.
derive_new_contents_from_chunks:
text := fs.read_file_text(path_abs) // utf-8, error if missing
lines := text.split('\n') // retains trailing '' if text ends '\n'
if lines.last() == "": lines.pop() // normalize off trailing-\n artifact
replacements := compute_replacements(lines, chunks)?
new_lines := apply_replacements(lines, replacements)
if new_lines.last() != "": new_lines.push("") // re-add trailing newline
return new_lines.join('\n')
Postcondition: every update produces a file ending in exactly one \n,
regardless of whether the input had one. Reimplementations MAY choose to
preserve "no trailing newline" when present — doing so is a deliberate
deviation.
6.5 Computing replacements
line_index := 0
replacements := []
for each chunk:
# 1. If the chunk has a "@@ ctx" marker, locate that line first.
if chunk.change_context.is_some():
idx := seek_sequence([ctx], lines, start=line_index, eof=false)?
line_index := idx + 1 # search for old_lines AFTER ctx
# 2. Pure-addition chunks (old_lines empty):
if chunk.old_lines.is_empty():
insertion_idx := (if lines.last() == "" then lines.len() - 1
else lines.len())
replacements.push((insertion_idx, 0, chunk.new_lines))
continue
# 3. Normal replacement: match old_lines in the file.
pattern := chunk.old_lines
new_slc := chunk.new_lines
found := seek_sequence(lines, pattern, line_index, chunk.is_end_of_file)
if found.is_none() and pattern.last() == "":
pattern := pattern[..pattern.len()-1] # drop trailing empty sentinel
if new_slc.last() == "":
new_slc := new_slc[..new_slc.len()-1]
found := seek_sequence(lines, pattern, line_index, chunk.is_end_of_file)
match found:
Some(start): replacements.push((start, pattern.len(), new_slc))
line_index := start + pattern.len()
None: error "Failed to find expected lines in <path>:\n<old_lines joined>"
replacements.sort_by_start_index()
Key invariants:
- Context (
@@ ctx) is matched by one line; it is a locator only and is never modified. After a successful context match, the old_lines search begins immediately after the context line. - A chunk's
old_linessearch starts atline_index(the cursor after the previous chunk), so chunks must appear in file order. - Pure-addition chunks (no
-orlines, only+) append at the end of the file. They do NOT honorline_index; they always go to the end. - The "trailing empty sentinel" retry exists because unified-diff-style
tools often emit a blank line at the end of
old_linesrepresenting the file's terminal newline. Our line-splitting strips that element from the file, so a literal match fails; we retry with the sentinel removed.
Replacements are then applied in reverse order of start_index so
earlier edits do not shift later edits' indices:
apply_replacements(lines, replacements):
for (start, old_len, new_seg) in replacements.reversed():
delete lines[start .. start + old_len]
insert new_seg at position start
6.6 seek_sequence — the fuzzy matcher
Implementation: codex-rs/apply-patch/src/seek_sequence.rs.
Signature:
fn seek_sequence(
lines: &[String], pattern: &[String],
start: usize, eof: bool,
) -> Option<usize>
Contract:
- Returns the smallest
i ≥ search_startsuch thatlines[i..i + pattern.len()]matchespatternunder one of four match predicates (tried in order).search_start = lines.len() - pattern.len()ifeofand the pattern fits, elsestart. - Empty
pattern→Some(start). pattern.len() > lines.len()→None(MUST NOT panic).
The four match predicates, tried in order (first success wins):
- Exact.
lines[i + k] == pattern[k]for all k. - Rstrip.
lines[i + k].trim_end() == pattern[k].trim_end(). - Full trim.
lines[i + k].trim() == pattern[k].trim(). - Unicode-normalized trim. Trim, then fold common typographic punctuation to ASCII, then compare.
Normalization table (MUST be implemented identically):
| Folded to | Source code points |
|---|---|
- |
U+2010 HYPHEN, U+2011 NON-BREAKING HYPHEN, U+2012 FIGURE DASH, U+2013 EN DASH, U+2014 EM DASH, U+2015 HORIZONTAL BAR, U+2212 MINUS SIGN |
' |
U+2018, U+2019, U+201A, U+201B |
" |
U+201C, U+201D, U+201E, U+201F |
(space) |
U+00A0 NBSP, U+2002, U+2003, U+2004, U+2005, U+2006, U+2007, U+2008, U+2009, U+200A, U+202F, U+205F, U+3000 |
All other code points are passed through unchanged. This lets the model emit ASCII hyphens/quotes/spaces even when the source file contains typographic variants (e.g. an em-dash pasted from a doc).
Per-chunk EOF hint. When eof == true (set from is_end_of_file), the
matcher first tries to match at the tail of the file
(i = lines.len() - pattern.len()) before falling through to the normal
forward search from start.
What's deliberately not supported.
- No "floating" / best-effort match. If all four passes fail, the chunk fails; there is no nearest-match heuristic.
- No matching across non-adjacent lines — the pattern must appear as a contiguous block.
- No multi-match disambiguation: the first match wins. Chunks must
carry enough context (or a
@@ header) that the first hit at or afterline_indexis the intended one.
7. Path resolution
cwdis theAbsolutePathBufpassed toapply_patch(...). In the CLI / harness it defaults to the process working directory, possibly further qualified by a workdir extracted from acd X && apply_patch <<EOFshell invocation (§8.3).resolve_path_against_base(path, cwd):- If
pathis absolute →path(cwd is ignored). - If
pathis relative →cwd.join(path).
- If
- The patch grammar does not define an escape mechanism. Paths containing spaces, tabs, or Unicode are supported as-is (the parser takes the full rest of the header line); paths containing literal newlines are unrepresentable by construction.
- A
FileSystemSandboxContextMAY be passed in; when present, every filesystem call is routed through it. All ofread_file_text,write_file,remove,get_metadata,create_directoryreceive the sandbox. The sandbox is responsible for enforcing path restrictions — the applier does no check of its own.
8. Invocation forms
The applier accepts the patch text through several transport layers. A reimplementation only strictly needs §8.1 (tool arg) — the others exist for historical compatibility.
8.1 Direct tool argument (freeform or JSON)
The preferred form. Either the freeform tool's input or the JSON tool's
input string is the full patch, e.g.:
*** Begin Patch
*** Add File: hello.txt
+Hello, world!
*** End Patch
8.2 Heredoc-wrapped
When the patch is invoked via shell (the legacy path), the model wraps
the patch in a heredoc. The parser's lenient mode strips the outermost
heredoc wrapper:
<<EOF
*** Begin Patch
...
*** End Patch
EOF
The opener must be one of <<EOF, <<'EOF', <<"EOF"; the closer must be
EOF on its own line. Mismatched quoting (<<"EOF') or a missing closer
is rejected.
8.3 Shell script with workdir
The harness also recognizes a cd <path> && apply_patch <<'EOF' ... EOF
shell invocation (parsed via Tree-sitter in
codex-rs/apply-patch/src/invocation.rs). The <path> is extracted into
ApplyPatchArgs.workdir and used to qualify cwd before applying hunks.
Any other pre- or post-commands cause the parse to fail over to "treat as
a regular shell command" rather than apply_patch.
8.4 stdin (standalone executable)
codex-rs/apply-patch/src/standalone_executable.rs lets the binary be
invoked as apply_patch with the patch on argv[1], OR with no args and the
patch piped on stdin.
9. Result presentation
9.1 Success
After a successful apply, the caller renders a git-style summary
(codex-rs/apply-patch/src/lib.rs::print_summary):
Success. Updated the following files:
A <added path 1>
A <added path 2>
M <modified or renamed path>
D <deleted path>
- Sections appear in the order Added / Modified / Deleted.
- Paths are the ones spelled in the patch (not canonicalized).
- Renamed files appear under
Mwith the original path, not the destination. - Exit status 0.
9.2 Failure
- Parse errors: written to stderr as
Invalid patch: <message>orInvalid patch hunk on line <N>: <message>. - Apply errors (context miss / old_lines miss / IO): written to stderr
with the Rust
anyhowchain, e.g.:Failed to find context '<ctx>' in <path>Failed to find expected lines in <path>:\n<block>Failed to read file to update <path>: <io err>Failed to write file <path>: <io err>Failed to delete file <path>: <io err>Failed to remove original <path>: <io err>Failed to create parent directories for <path>: <io err>
- Exit status 1 (apply/parse failure) or 2 (argv usage error).
9.3 Harness-side tool call result
When invoked through the harness, the handler wraps the above in
ExecToolCallOutput { exit_code, stdout, stderr, aggregated_output, duration, timed_out }. The model sees aggregated_output.
9.4 Progress events
The PatchApplyUpdatedEvent is emitted to the TUI as each hunk is applied
(only when the progress feature is on). This is a UX detail and is not part
of the observable patch semantics.
10. Edge cases (test-derived)
| Case | Behavior |
|---|---|
| Patch with zero hunks | Error: No files were modified. |
| Add File overwriting an existing file | Silent overwrite. |
| Delete File on a directory | Error: path is a directory. |
| Delete File on a nonexistent file | Error propagated from fs.get_metadata / fs.remove. |
| Move to an existing destination | Destination overwritten; source removed. |
| Update File with 0 chunks | Parse error: Update file hunk for path '<p>' is empty. |
Chunk with only + lines (pure addition) |
Inserts at end of file (before final empty line if any). |
Chunk whose old_lines end in an empty string |
Retry without the trailing empty; lets EOF edits match. |
Patch with *** End of File marker |
is_end_of_file = true; matcher tries tail-of-file first. |
| Unicode dash/quote/NBSP mismatch between patch and file | Normalized-trim match (4th seek pass) matches. |
| Leading/trailing whitespace on a sentinel line | Ignored (line.trim() before marker compare). |
| Blank line inside an Update File between chunks | Ignored (used as visual separator). |
| Heredoc wrapper around the whole patch | Stripped in Lenient mode. |
Streaming: *** End Patch absent yet |
OK in parse_patch_streaming; last incomplete hunk is dropped. |
First chunk of an Update File lacks @@ |
Allowed (context-less first chunk). |
Non-first chunk missing @@ |
Parse error. |
| Absolute path in patch | Accepted by parser; model is told not to emit these. |
| Multiple chunks touching the same file | Applied in reverse start-order; must be in file-order in the patch. |
| One hunk of N fails | Prior hunks remain applied; later hunks skipped. |
| File with no trailing newline as input | Output gains one (post-condition). |
11. Reimplementation checklist
To reimplement this format end-to-end, a conforming implementation MUST:
- Accept the exact sentinel tokens in §3.2 with the trailing space
where required; match marker lines after
trim()only. - Parse the grammar in §3.1 including the context-less first chunk
allowance, the
*** End of Fileterminator, blank-line separation between chunks, and the*** Move to:renames. - Implement Lenient mode (heredoc strip) and Streaming mode as described in §4.3.
- Emit the error messages in §4.5 verbatim (test compatibility).
- For Update File, read the target with UTF-8, split on
\n, drop the trailing empty element, apply chunks via the replacement machinery in §6.5, and re-add a trailing newline before writing. - Implement
seek_sequencewith the four-pass strictness hierarchy and the exact Unicode normalization table in §6.6, including the pattern-longer-than-input →Noneguard and theeoftail-first search. - Apply hunks sequentially and non-atomically; do not rollback on partial failure.
- Silently overwrite existing destinations for Add File and Move.
- Emit the
Success. Updated the following files:/A/M/Dsummary in §9.1 on success, and stderr messages in §9.2 on failure. - Register both freeform-grammar and JSON-function tool variants with
supports_parallel_tool_calls = false. - Ship the agent prompt in §2 verbatim.
Optional / harness features (not required for correctness):
- Heredoc
cd <dir> && apply_patch <<'EOF' ... EOFshell-form detection with workdir extraction. - Streaming progress events to the UI.
- Unified-diff rendering (
unified_diff_from_chunks) for displaying a user-visible diff after apply.