Skip to content

Instantly share code, notes, and snippets.

@monperrus
Last active August 16, 2026 17:57
Show Gist options
  • Select an option

  • Save monperrus/2a9f682f3fca18de5693f0975bf5b396 to your computer and use it in GitHub Desktop.

Select an option

Save monperrus/2a9f682f3fca18de5693f0975bf5b396 to your computer and use it in GitHub Desktop.
`apply_patch`: OpenAI's freeform patch tool, its transport, and its grammar

apply_patch: OpenAI's freeform patch tool, its transport, and its grammar

OpenAI's Codex CLI (and the GPT-5 model family more broadly) ships a tool called apply_patch for editing files. Unlike almost every other tool in the ecosystem, its argument isn't JSON — it's raw text, constrained by a formal grammar. This note covers what that means, exactly how it's wired into the API, and what happens when you actually probe it against real endpoints.

Why a non-JSON tool exists at all

Classic tool calling asks the model to emit a JSON object as the tool's arguments. For short, flat arguments that's fine. It gets expensive for large, syntax-heavy, multi-line content — a diff, a source file, a SQL query — because every embedded quote, backslash, and newline has to be escaped correctly across potentially hundreds of lines. The failure mode is that models drop or mangle escapes under length pressure, and the payload fails to parse.

OpenAI's answer, introduced alongside GPT-5, is a second tool type: custom tools. A custom tool's output is raw text instead of a JSON object — the model writes x = "old value" literally, not x = \"old value\". Optionally, that raw text can be constrained by a grammar (Lark or regex), which goes a step further than just avoiding escaping: with format: {type: "grammar", ...}, the API does constrained decoding, restricting the token sampler to only ever produce tokens the grammar allows. A malformed patch becomes structurally impossible to emit, not just less likely — a materially stronger guarantee than JSON Schema, which is advisory (the model can still emit invalid JSON you have to detect and reject).

apply_patch is the flagship example of this: a tool literally described in OpenAI's own source as "well-suited for GPT-5 models," built as a freeform/grammar custom tool from day one.

Documentation:

The apply_patch grammar

A patch is wrapped in a *** Begin Patch / *** End Patch envelope containing one or more file directives:

*** 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
  • *** Add File: <path> — every following line is a + line: the new file's full content.
  • *** Delete File: <path> — nothing follows.
  • *** Update File: <path> — optionally followed by *** Move to: <path> (rename), then zero or more hunks.
  • Each hunk starts with @@ (optionally followed by a context anchor, e.g. a function signature, when disambiguation is needed in a larger file), then -/+/ (context) lines, and may end with *** End of File.

This is not unified diff. There's no --- a/file / +++ b/file header pair, no @@ -l,s +l,s @@ line-number/hunk-size metadata — the @@ marker carries no numbers at all, just an optional textual anchor. The file action and path live in the *** directive line instead of the header.

The grammar

Source of truth: apply_patch.lark in openai/codex, upstream (unmodified):

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

apply_patch isn't the only grammar-based freeform tool in codex. There's exactly one other: exec (the "code mode" tool, PUBLIC_TOOL_NAME = "exec" in codex-rs/core/src/tools/code_mode/execute_spec.rs), whose grammar is almost unconstrained:

start: pragma_source | plain_source
pragma_source: PRAGMA_LINE NEWLINE SOURCE
plain_source: SOURCE

PRAGMA_LINE: /[ \t]*\/\/ @exec:[^\r\n]*/
NEWLINE: /\r?\n/
SOURCE: /[\s\S]+/

It only recognizes an optional // @exec:... pragma line before raw source code — there's essentially nothing to validate structurally. Its entire purpose is avoiding JSON-escaping cost for arbitrary source code, not enforcing shape (unlike apply_patch, which does both).

A gotcha if you want to validate this locally with Python's lark

The upstream grammar uses zero-width regex terminals — /(.*)/ can match an empty string. Python's lark package's Earley engine flatly rejects grammars containing those (GrammarError: Dynamic Earley doesn't allow zero-width regexps, in every parser/lexer combination — earley+basic, earley+dynamic_complete, and lalr all fail the same way). This appears to be a lark-Python-specific restriction, not a property of the grammar itself (Codex's own Rust implementation obviously accepts it fine).

A fix is available at <lark-parser/lark#1639

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment