Edit Tools: apply_patch vs Edit
Chord provides two complementary tools for editing files, optimized for different model training backgrounds.
How to use this page
Section titled “How to use this page”- Pick a format: Quick Comparison and Tool Selection: which tool Chord sends by default, and how to override it.
- Wire shapes: apply_patch Tool (Codex envelope) and Edit (Replace) Tool document the exact envelopes and matching rules.
- Day to day: Recommended Workflow and Task-Specific Guidance.
- Approvals: Permissions covers write scope and auto-approval.
Quick Comparison
Section titled “Quick Comparison”| Feature | apply_patch Tool | Edit (Replace) Tool |
|---|---|---|
| Format | Codex patch envelope (*** Begin Patch … *** End Patch) with @@ hunks |
Text matching (old_string → new_string) |
| Best for | Models trained with OpenAI’s apply_patch |
Models trained with Claude Code or similar replace interfaces |
| Scope | Multiple files per call: add, update, delete, move | One existing file per call |
| Position control | Context lines + optional header anchors | Exact string matching |
| Multi-occurrence | N/A (context-driven) | replace_all parameter |
| Typical models | gpt-5.5, gpt-5.3-codex, codex-auto-review | Claude, Qwen, GLM, MiniMax, DeepSeek, Gemini |
Tool Selection
Section titled “Tool Selection”Chord automatically selects the appropriate tool based on the active model:
- gpt-5 and later gpt major families (gpt-5, gpt-5-mini, gpt-5-nano, gpt-5-codex, any
gpt-5.*name, and later majors like gpt-6-astra) andcodex-auto-review→apply_patch(Codex envelope) - All other models (gpt-3.5, gpt-4/4o, gpt-oss-*, o-series, Claude, Qwen, GLM, DeepSeek, Gemini, …) →
edit(old_string/new_string)
The gpt-4/4o, gpt-3.5, and o-series families are not patch native: the apply_patch tool did not exist when they were trained (it was introduced with GPT-5 in August 2025), and measured results are negative or untrained. Every gpt family from gpt-5 onward defaults to the patch tool surface, matching the Codex model catalog; this includes later majors such as gpt-6-astra, since apply_patch is first-party Codex training data that stays in OpenAI training across generations. A model that does not carry the patch signal can be opted out with compat.apply_patch.enabled: false.
When a patch-native model keeps apply_patch, Chord also hides write and delete: the envelope subsumes them (*** Add File: creates, *** Delete File: removes), matching the native Codex CLI surface those models are trained on. Fallback pairings keep write/delete: a non-patch-native model that only got apply_patch because edit is disabled still sees them, and a patch-native model downgraded to edit needs write to create files at all.
Freeform (custom tool) emission
Section titled “Freeform (custom tool) emission”On OpenAI-compatible Responses endpoints, a gpt-5-and-later family model or codex-auto-review additionally receives apply_patch as a freeform custom tool (type: "custom" with a Lark grammar), instead of a JSON function tool. Chord sends the grammar in the request’s format.definition field; when the server supports constrained decoding, it restricts the patch protocol during model generation. The grammar currently matches Codex’s definition and requires at least one file operation, non-empty added-file content, and valid patch-line structure.
The client still validates and executes the returned text with its own parser, so unconstrained responses (from a gateway that strips or rewrites the grammar, or from a server that does not enforce it) retain a tolerant fallback path. Grammar cannot determine whether context came from the latest file or whether the requested change is semantically correct. All other models receive the JSON function shape, and non-Responses endpoints always use the function shape (they have no custom tool type).
Hosts that accept Responses requests but reject custom tools do not get a built-in exception: a patch-native model there will emit the freeform shape by default, and the gateway rejects it with an actionable error. Set compat.apply_patch.freeform: false for such hosts to force the JSON function shape.
Overriding the defaults
Section titled “Overriding the defaults”Every default above can be overridden per provider or per model under compat.apply_patch (three-state: omit to keep the inference):
providers: my-relay: type: responses compat: apply_patch: enabled: true # tool surface: keep apply_patch (hide edit + write/delete) freeform: false # wire shape: JSON function tool, not custom openai: type: responses models: gpt-5.5: compat: apply_patch: freeform: false # model-level override: name looks freeform but the gateway is notenabled: trueadopts the full patch-native semantics for any model (patch kept,edit/write/deletehidden, patch-only prompt guidance).enabled: falseforces the edit surface even for patch-native models.freeform: trueforces the custom tool shape;freeform: falseforces the JSON function shape.
If a gateway lowers a custom tool into {"input": "..."} instead of {"patch": "..."}, Chord reports an actionable error pointing at compat.apply_patch.freeform: false; set it and the request will be sent as a function tool.
apply_patch Tool (Codex envelope)
Section titled “apply_patch Tool (Codex envelope)”Format
Section titled “Format”For the Responses freeform shape, the Lark grammar is sent to the server as a generation constraint with the custom tool, so it constrains the patch protocol skeleton on the server side: the complete patch has at least one file operation, added files have at least one + content line, and update chunks follow Codex’s line and hunk structure. It cannot verify that a file anchor is unique, that the file was unchanged after reading, or that the edit is semantically correct.
Chord still revalidates and executes the returned text with its client parser and transactional executor, so a missing or unenforced grammar leaves the client responsible for rejecting invalid results or applying its compatibility rules.
The single patch argument carries the Codex patch body. Chord accepts the normal complete envelope and also repairs a missing *** Begin Patch and/or *** End Patch wrapper before parsing. Inside the body, you can include any number of file operations:
*** Begin Patch*** Update File: src/main.go@@ func main() { context line-removed line+added line context line*** Add File: docs/new.md+# New document+First line.*** Delete File: tmp/old.txt*** End PatchSupported operations:
*** Add File: path: create a file; every body line starts with+.*** Update File: path: modify a file with one or more@@hunks.*** Move to: newpath: rename while updating; must appear directly after its*** Update File:line. A pure rename still needs one (possibly context-only) hunk.*** Delete File: path: remove a file; no body.*** End of File: after a hunk, pins that hunk to the file tail (useful when the same block also appears earlier).
Hunks apply in order; each hunk is matched at the first position after the previous hunk’s application point. Repeated plain *** Update File: sections for the same normalized path follow Codex ordering semantics: each section patches the previous section’s in-memory result, and that file is committed as one mutation (so a later mismatch leaves that file unchanged rather than exposing Codex’s partial-write behavior).
Lines inside a hunk keep their raw ' '/+/- prefix, so file content that itself begins with *** followed by a space stays ordinary context; only lines beginning with an unprefixed *** followed by a space are protocol markers.
When to Use
Section titled “When to Use”- Your model has been trained with OpenAI’s
apply_patchor similar patch-based interfaces - The change spans several files, or creates/deletes/moves files alongside content edits
- You need precise positional control through context lines
Example
Section titled “Example”{ "patch": "*** Begin Patch\n*** Update File: main.go\n@@\n func main() {\n-\tfmt.Println(\"hello\")\n+\tfmt.Println(\"hello, world\")\n }\n*** End Patch"}Optional Header Anchors
Section titled “Optional Header Anchors”You can add text after @@ to help locate ambiguous blocks:
@@ func processUser(id int) error { if id < 0 {- return errors.New("invalid")+ return fmt.Errorf("invalid user ID: %d", id) }Important: Only use headers you’ve verified exist in the file. Headers are soft anchors: when the header text is not found, matching falls back to the hunk body alone.
Line Endings
Section titled “Line Endings”Hunk lines are matched without their line endings, so the LF text returned by read applies to files with any line-ending convention. Untouched lines keep their original line endings. In a file that uses CRLF or CR throughout, added lines use that ending too; in a file that mixes line endings, an added line takes the ending of the line it replaces or sits next to. The updated file always ends with a line ending.
Transactional Behavior
Section titled “Transactional Behavior”All operations in one envelope are planned from a single filesystem snapshot before any file is modified. Envelope-wide preflight failures (such as malformed syntax, unsafe overlapping paths, or an unreadable snapshot) leave every file unchanged. An operation-level failure, such as a missing update source or an existing Add target, rejects that file group while independent file groups can still commit.
If any planned file changes on disk before commit, the commit is rejected without writing its successful subset. If a write fails mid-commit, already-written mutations from that commit attempt are rolled back.
Atomicity is per file, not per envelope. Each file is an independent unit: all operations that touch one file (including repeated *** Update File: sections for that same path) commit together, or are rolled back together. When one file fails the other independent files in the same envelope are still applied and written to disk.
The failure result lists the committed changes (which do not need to be redone), explains which operation groups were not applied and why, and tells you to rebuild the failed operations from current file contents and resubmit only those. Resolve each reported failure and submit the rebuilt operations against the current workspace; do not re-emit committed files, and the result does not repeat the submitted patch.
A failed file drags its whole group: if an earlier operation on the same file matched in memory but a later one failed, all of that file’s operations are reported as unapplied and omitted from the final plan. Earlier successful prerequisite groups remain eligible to commit, while groups that depend on the discarded group are omitted with it. The result lists every operation carried along (including the ones that matched), so the model can rebuild the complete failed dependency chain from its own submitted patch.
A move binds both its source and destination into the same dependency boundary. If the move fails, later operations touching either path are also rejected and included in the unapplied operations. The same rule applies when a source group fails after an earlier move appeared to succeed: operations that depended on the moved destination are rolled back with it. This keeps the failure complete instead of reporting a dependent destination edit as committed after its prerequisite was discarded, so a rebuilt operation does not miss this dependency chain.
When the patch fails without an applied diff, the tool card keeps the requested-patch preview and labels it separately from the error; it switches to the final diff once execution completes successfully.
Error Messages
Section titled “Error Messages”- “hunk not found (N/M)”: The indicated hunk does not match the current file. The error identifies the first expected complete line, or labels it as a prefix when the diagnostic preview is truncated. When available, it also explains that the text occurs only within a longer line or earlier than the preceding hunk. If earlier hunks of the same file matched in memory but a later one failed, none of that file group’s hunks were applied. Re-read the target range, rebuild the failing hunk from current complete lines, and keep the group’s other hunks with it when you resubmit.
- “cannot add file that already exists”:
*** Add File:targets an existing path; use*** Update File:instead. - “apply_patch contains overlapping operations”: Two operations in one envelope touch paths where one contains the other (for example
diranddir/file), or resolve to the same file through different names; merge them into one operation. Repeated*** Update File:sections for the exact same path are allowed and apply in order. - “changed after planning”: The file was modified between validation and commit; nothing was written; retry against the current content.
- “apply_patch partially applied: N changes committed, M file groups not applied: …”: One or more independent changes committed while other operation groups were omitted (the singular form uses “change” / “file group”). The changes under “Applied patch” are already on disk; do not redo them, and the failure does not echo the submitted patch back. “Not applied” lists each omitted operation group’s path and cause; resolve each cause, rebuild those operations from current file contents, and submit only those.
Edit (Replace) Tool
Section titled “Edit (Replace) Tool”Format
Section titled “Format”{ "path": "main.go", "old_string": "fmt.Println(\"hello\")", "new_string": "fmt.Println(\"hello, world\")", "replace_all": false}When to Use
Section titled “When to Use”- Your model hasn’t been specifically trained on patch formats
- The change is straightforward: find exact text → replace with new text
- You want to rename a variable/identifier across a file (
replace_all: true)
Parameters
Section titled “Parameters”old_string(required for a single replacement): Exact text to find. Must match indentation, whitespace, and newlines exactly. As a last-resort fallback, punctuation variants are tolerated (see Punctuation Tolerance below).new_string(required for a single replacement): Replacement text.replace_all(optional):trueto replace all occurrences,false(default) to replace only the first.
Several replacements in one file
Section titled “Several replacements in one file”Use edits for disjoint changes in one call:
{"path":"server.go","edits":[{"old_string":"const port = 8080","new_string":"const port = 3000"},{"old_string":"const retries = 2","new_string":"const retries = 3"}]}Each entry has old_string, new_string, and optional replace_all. Do not combine edits with top-level replacement fields. Entries that request changes match the original file, so one entry cannot target text introduced by another. Batch matching is exact after argument character cleaning and line-ending adaptation; it does not use trailing-newline or punctuation/whitespace tolerance. Any failing entry rejects the whole batch before writing, and the result names every failing entry with its reason in one report, so fix all of them and resend the complete batch. Entries whose old_string and new_string are identical request no change: they are skipped without checking whether that text exists in the file and never counted as replacements, and a batch with nothing to change writes nothing and reports no changes. A successful batch writes once and reports diagnostics once.
Example: Single Replacement
Section titled “Example: Single Replacement”{ "path": "server.go", "old_string": "const port = 8080", "new_string": "const port = 3000"}Example: Rename Variable
Section titled “Example: Rename Variable”{ "path": "handler.go", "old_string": "userID", "new_string": "userId", "replace_all": true}Error Messages
Section titled “Error Messages”- “old_string not found in file”: The exact text doesn’t exist even under punctuation tolerance. Check whitespace, indentation, and newlines. When the mismatch is a character-level difference (a dropped or extra rune), the error also points at the closest matching block in the file (its line number, how similar it is, and the exact differing lines) so you can see the one-character mistake (for example a missing
)or a doubled,,) without re-reading the whole file. When whole lines have drifted so far that the shown lines cannot rebuild the target, the error instead names the drift and hands overreadcoordinates (offset/limit) for the closest-match range, or suggests a smaller 2-4 line anchor. - “old_string found N times”: Multiple matches found. Either:
- Add more context to make it unique
- Set
replace_all: trueif you want to replace all occurrences
- “old_string and new_string are identical”: A single edit reports this as an error. A batch skips the entry and lists it as skipped; a batch with no text change reports no changes and writes nothing.
When the same target file repeatedly fails approximate matching on edit/apply_patch, the agent appends a note to the model-visible result (from the second failure on) telling it to read the target range fresh, or switch to write for a whole-block replacement, instead of retyping the same old text from memory. The note does not appear in the UI, and any successful result or a new turn resets the count.
Invisible Character Cleaning
Section titled “Invisible Character Cleaning”The write paths of edit, apply_patch, and write strip zero-width formatting characters and floating combining marks that models leak into tool arguments (zero-width space, zero-width non-joiner, zero-width joiner outside emoji sequences, word joiner, mid-stream BOM, soft hyphen; and a diacritic with no visible base, such as a stray macron sitting after a space instead of on a letter). These runes carry no content, and a floating mark cannot change what any character means, so stripping them cannot change what the text means; leaving them in would plant invisible bytes in the file.
When any are removed, the tool result reports exactly which code points were cleaned (for example U+200B×2, U+0304×1), so the model learns to stop emitting them. A combining mark over any visible base is kept (letter, digit, symbol, or punctuation) so legitimate diacritics (Vietnamese/Arabic/Devanagari text, stacked marks) and sequences such as a U+0305 overline over a digit in math notation survive untouched.
In apply_patch the clean is limited to the lines the patch adds: existing content elsewhere in the file is never scanned or rewritten by this clean.
Single and batch replacements accept the LF text returned by read. For files with uniform CRLF or CR line endings, the replacement keeps the file’s line-ending convention. For files that mix line endings, each line break in old_string matches any line ending, and the replacement uses the line ending of the block it replaces. Exclude the READ_RESULT metadata line from replacement text.
Trailing Newline Tolerance
Section titled “Trailing Newline Tolerance”The tool automatically handles minor trailing newline differences:
- If
old_stringhas a final\nbut the match doesn’t (or vice versa), and the match is unique, the edit proceeds. - This reduces retries caused by newline mismatches.
Punctuation Tolerance
Section titled “Punctuation Tolerance”When exact matching and trailing-newline matching both fail, the tool retries with common punctuation variants treated as equivalent, the same 1:1 normalization surface apply_patch uses:
- Curly vs straight quotes (
“ ”↔" ",‘ ’↔' ') - Dashes (
–,—,−↔-) - Full-width vs half-width CJK punctuation (
,;:.!?())
The fallback applies only when the normalized old_string has one unique match, reports its use in the tool result, and preserves the file’s original punctuation for unchanged context. Multiple normalized matches error with the “found N times” message.
A single space directly adjacent to a separator punctuation mark is also treated as optional: : and : with a trailing space (and :the when the space is dropped) match the same text, as does an inter-word space (diff and and diffand). This covers models that tokenize ": " as one token and re-emit it as :, or drop/insert a word-boundary space.
The folding is deliberately narrow: only one space right after , ; : . ! ? ( (or right before )) or between two word characters is optional. Double spaces, spaces after quotes or dashes, indentation, and newlines stay significant, so a genuine layout mismatch still fails with “old_string not found” instead of silently applying a wrong edit.
The result text reports when the tolerance was used; the tool description deliberately does not advertise it, so models still aim for exact matches.
A combining mark with no visible base (at the start of a line or preceded only by whitespace) is folded out during this normalization: it is a tokenizer artifact that never exists in real file content at that position, but does leak into copied text when a tokenizer splits a heading like ### [U+0304].2.1. A mark over any visible base (letter, digit, symbol, or punctuation) is kept untouched, so legitimate diacritics (Arabic, Devanagari, Vietnamese, including stacked sequences) and marks on digits or symbols (math overlines) are never folded away.
Recommended Workflow
Section titled “Recommended Workflow”Neither edit tool requires a prior read: both tools read current on-disk content at execution time. For reliable edits, still follow these recommendations:
- Inspect the target area first when you have not already verified the exact text, path, or hunk anchor.
readorgrepare good ways to do that. - Use the smallest unique block (2-4 lines). Large context blocks are more likely to become stale.
- Re-read after failures. If a hunk or string match fails, the file may have changed; read it again before retrying.
Task-Specific Guidance
Section titled “Task-Specific Guidance”For Localized Changes
Section titled “For Localized Changes”Both tools work well. Choose based on model training:
- apply_patch: Better when you need positional control (e.g., “change the first occurrence in this function”).
- Edit: Better for simple find-replace with clear boundaries.
For Renaming/Refactoring
Section titled “For Renaming/Refactoring”- Edit with
replace_all: true: Rename a variable across one file. - Shell with the language’s refactoring command (such as
gopls rename): For symbol-aware renames across multiple files.
For Large-Scale Changes
Section titled “For Large-Scale Changes”- Creating, deleting, or moving files →
apply_patchdoes this natively (*** Add File:/*** Delete File:/*** Move to:); models onedituse Write and Delete - Batch text replacements across many files → Use Shell with
sdorsed - Symbol renames across files → Use Shell with the language’s refactoring command
Permissions
Section titled “Permissions”Both tools share the file permission family (path-based authorization). A single approval for a path applies to both editing tools.
In permission rules, hook filters, and skill allowed_tools, the formal names are edit and apply_patch. patch is accepted as a legacy alias for apply_patch, so existing configurations keep working.
Permission Configuration
Section titled “Permission Configuration”Configure permissions for either edit tool name; a rule for one editor applies to the other editor unless the other editor also has an explicit rule:
Unified Configuration (recommended):
permission: edit: allow # Both apply_patch and edit tools allowedDisable One Format (advanced):
permission: edit: allow apply_patch: deny # patch-native models (gpt-5 family/codex-auto-review) fall back to editPermission Fallback Rules:
- If only
editis configured,apply_patchinherits the same permission - If only
apply_patchis configured,editinherits the same permission - This includes
deny:edit: denydisablesapply_patchtoo unlessapply_patchalso has its own explicit rule - If both are configured, each tool uses its own explicit rule
- A single
editorapply_patchrule applies to both tools and overrides wildcard rules
Examples:
edit: allow→ both tools allowed; patch-native models (gpt-5 family/codex-auto-review) normally seeapply_patch, other models normally seeeditedit: allow, apply_patch: deny→ apply_patch denied, edit allowed; patch-native models fall back toeditapply_patch: allow, edit: deny→ apply_patch allowed, edit denied; non-GPT models fall back toapply_patch*: deny, apply_patch: allow→ both tools allowed (apply_patch rule is inherited by edit)*: allow, apply_patch: deny→ both tools denied (edit inherits the apply_patch deny)
Technical Notes
Section titled “Technical Notes”Why Two Tools?
Section titled “Why Two Tools?”edit locates a local replacement by its original text; apply_patch describes additions, updates, moves, and deletions as a patch. Chord selects the tool for the model, so manual tuning is usually unnecessary. If a model or endpoint cannot use the default format, override the selection with compat.apply_patch.enabled.
Matching Tolerance
Section titled “Matching Tolerance”apply_patch matches hunk context in three exact passes: exact match first, then ignoring trailing whitespace, then ignoring surrounding whitespace.
Punctuation/whitespace tolerance (quotes, dashes, full-width CJK punctuation, and the optional space after separator punctuation) is deliberately not a fourth pass: it is a single separate step that must land in exactly one place, and an ambiguous tolerant match is rejected with the candidate lines named instead of silently taking the first one. Repeated blocks still need enough nearby context (or an *** End of File marker) to make the intended location clear.
For any file that can be decoded as text, a final fallback also treats common Chinese and ASCII punctuation as equivalent, and, like the edit tool, treats a single space adjacent to a separator punctuation mark as optional (:, : followed by a space, and :the match the same line). Both tools share the same normalization and the same preservation rules. This includes source files, dotenv files such as .env.example, and extensionless text files. The fallback applies only when the complete hunk has one unique match.
It preserves punctuation from the current file in unchanged parts of replacement lines and reports its use in the tool result. Ambiguous matches are rejected, and a fragment occurring inside a longer line is diagnostic only, not an automatic substring edit. Binary or otherwise undecodable files do not enter this fallback because text decoding fails before hunk matching.
Q: Can I force a specific tool?
A: Yes. Set compat.apply_patch.enabled: true for patches or false for replacements. Usually, leave the automatic selection in place.
Q: What if my model isn’t recognized?
A: By default, unrecognized models use the edit (replace) tool. gpt-5-and-later family names (gpt-5, gpt-5-mini, gpt-5-nano, gpt-5-codex, any gpt-5.* name, later majors like gpt-6-astra) and codex-auto-review use apply_patch; you can override any model via compat.apply_patch.enabled.
Q: Do both tools support the same file types? A: Yes. Both work with any text file (detected encoding: UTF-8, UTF-16, GB18030, etc.). Binary files are rejected.
Q: Can I use both tools in the same conversation? A: Only one tool is visible at a time, based on the active model. You won’t see both simultaneously.
See Also
Section titled “See Also”- Tool Reference – All available tools
- Permission System – How file access control works
- LSP – Diagnostics after edits