Skip to content

Multi-file Edit / apply_patch Envelopes#

What it is#

This capability covers envelope-style patch tools: a single tool call that carries a self-describing text block capable of expressing several file operations at once — creating, updating, moving, and deleting files, and (within an update) multiple non-contiguous hunks — rather than a call whose arguments are scoped to one file and one old_str/new_str pair. The canonical shape is apply_patch: the model emits (or the harness parses) a patch body delimited by markers like *** Begin Patch / *** End Patch, containing per-file sections (*** Update File:, *** Add File:, *** Delete File:, *** Move to:) and, inside an update section, one or more @@ hunk headers with context lines. A closely related but distinct pattern is single-file multi-edit batching (multi_edit/multiedit): several find-and-replace operations against one file submitted as one atomic call, still using the plain old_str → new_str primitive underneath rather than a patch grammar.

This sits adjacent to, but is explicitly distinguished from, plain file edit (single targeted replacement). The distinguishing features that put a tool in this capability rather than ordinary file edit are: (a) the envelope can span multiple files in one call, (b) the format is a structured text grammar the harness parses (sometimes grammar-constrained rather than JSON-schema-constrained), and (c) some implementations treat the whole envelope as atomic — all hunks apply or none do.

In the coding-agent workflow, this capability is the model's mechanism for expressing a coordinated, multi-part change (e.g. renaming a symbol touched in three files, or restructuring a module into a new file layout) as a single tool round-trip instead of N sequential single-file edit calls. It trades JSON-schema simplicity for expressiveness and fewer round-trips, and in several implementations it exists specifically because certain model families were trained on or prefer this text format over structured JSON diffs.

Common implementation patterns#

Two genuinely different capabilities share this label. apply_patch-envelope tools are one mechanism choice for file editing generally, not a separate capability in their own right. Separately, multi_edit/multiedit is a batching capability layered on top of the ordinary single-file old_str/new_str primitive — it doesn't introduce a new patch grammar, it just lets several such replacements ride in one call.

Grammar-constrained vs. JSON-wrapped. Some implementations expose apply_patch as a grammar-constrained freeform tool (the model emits text matching a formal grammar directly, not a JSON string containing patch text) — a niche but deliberate choice, used specifically because patch formats are awkward to encode as JSON (escaping multi-line diff text inside a JSON string is lossy and verbose). Other implementations instead wrap the same envelope text as a plain string argument inside an otherwise-ordinary native function-calling tool — same grammar, different transport.

Model-gated routing is the dominant cross-cutting pattern for this capability specifically. Several implementations route to apply_patch only for a subset of model families and to a different single-file mechanism otherwise, treating the same edit capability as two distinct tools depending on which model is attached. This is one of the clearest examples of a harness switching tool identity, not just tool behavior, based on which model is loaded — the underlying assumption is that certain model families were trained on or otherwise prefer patch-envelope output over old_str/new_str JSON arguments.

Atomicity and staging semantics diverge. Some multi-edit implementations are explicitly all-or-nothing ("all edits fail if any one fails") with exact whitespace matching required. A staged-apply design takes atomicity furthest: file operations are represented as accumulated plan state rather than native tool calls at all, and none of them touch disk until the user explicitly applies the plan — the multi-file envelope is accumulated as a plan, not executed per-call. Other implementations apply the patch immediately per call (subject to the harness's ordinary sandbox/approval layer), with no staging step.

Availability varies: some implementations ship the envelope tool disabled by default and mutually exclusive with the ordinary editor, reflecting that a harness generally picks one edit mechanism per session/model rather than exposing both simultaneously.

Naming is comparatively convergent relative to other capabilities: apply_patch is used verbatim across a wide range of independent implementations, likely because the format itself (and its *** Begin Patch/*** End Patch markers) originated in one place and was adopted wholesale rather than reinvented.

Permission, sandbox & safety#

Because a multi-file envelope call can create, overwrite, move, and delete several files atomically in one round-trip, it concentrates more blast radius per tool call than a single-file edit — and every common implementation folds it into the same approval path as its ordinary write/edit tool rather than creating a distinct, more cautious gate for it specifically. Concretely: per-call-prompt implementations route the envelope through the same approval callback as any other write tool; configurable-approval-tier implementations apply their general policy/sandbox-mode settings uniformly to the envelope alongside the shell tool, with no patch-specific policy knob. A staged-apply design is the one pattern where this capability's blast radius is structurally contained rather than policy-contained — operations only ever stage diffs; nothing reaches disk until the user explicitly applies, which functions as the single review gate for the entire multi-file change at once (arguably a better fit for a capability whose whole point is coordinated multi-file mutation than a per-call yes/no prompt would be).

No dedicated sandboxing applies to the patch mechanism itself: OS-level sandboxes are scoped to process execution (shell/bash), not file mutation, so an apply_patch call inherits the same filesystem-write restrictions (working-directory scoping, .git write blocks) that apply to ordinary file writes, rather than anything specific to the multi-file case.

The one safety-relevant asymmetry worth naming: a multi-file envelope's larger surface area per call means a single approval ("yes, apply this patch") authorizes more simultaneous file-state change than a single-file edit approval would — implementations rarely summarize or diff-preview the full multi-file impact differently from a single-file edit before that approval, aside from a staged-apply plan-review step or the general diff-preview UI some IDE-embedded tools already show for ordinary edits.

Design considerations#

Convergent: (1) naming — apply_patch is used verbatim across most full-coverage implementations, unusually strong convergence; (2) model-gated routing — independent implementations route to apply_patch specifically for particular model families and to a different single-file mechanism otherwise, part of a broader model-aware tool-routing pattern; (3) permission treatment — every implementation folds this capability into its ordinary write/edit approval path rather than inventing a stricter one.

Divergent: (1) transport — grammar-constrained freeform vs. JSON-string-wrapped vs. pure prompt-injected text with no tool-call envelope at all; (2) atomicity model — immediate per-call application vs. staged/deferred until explicit apply; (3) scope — true multi-file, multi-operation-type envelopes vs. single-file multi-hunk batching only; (4) availability — some ship the envelope disabled by default, treating it as an alternative rather than a default capability, where most others enable it conditionally based on the attached model rather than a static flag.

This capability's growth tracks directly with adoption of models that were fine-tuned on or documented to prefer patch-text output — nearly every full-coverage implementation explicitly ties apply_patch availability to model identity rather than offering it as a model-independent alternative edit mode. That coupling — a tool whose existence in the exposed set depends on which model is loaded — is a sharper and more explicit version of the general "model-gated tool subsets" pattern, and is largely specific to this capability among file-mutation tools.

Implications for PluggableHarness Agent#

The tool reference catalog explicitly declines to make an apply_patch-style envelope the reference edit_file implementation: the filesystem/edit_file row recommends the old_str → new_str exact-match-replacement pattern as the reference implementation because it's a widely used choice that needs no grammar-constrained parsing, unlike apply_patch-style envelopes. That decision is well-supported by this capability's own evidence: the model-gated routing pattern above shows apply_patch isn't chosen on protocol-design merits so much as a specific-model-family compatibility shim — not a property PluggableHarness Agent's provider-agnostic tool protocol should bake into its one first-party reference operation. The reference catalog already makes the right call by choosing the simpler, JSON-schema-native mechanism and leaving apply_patch-style envelopes as a differentiator a third-party provider MAY implement (free to expose its own multi_file_edit operation with kind = resource and whatever risk fits its blast radius, per the existing edit_file moderate precedent).

Two things this evidence bears on:

  1. ConcurrencySpec.key_fields is single-path-shaped and would need extension for a genuine multi-file envelope operation. Its own example (safe: true, key_fields: ["path"]) implicitly assumes one call touches one resource key. A third-party multi_file_edit provider whose single Invoke call legitimately touches N files (as apply_patch's envelope does) can't express "serialize against every one of these N paths" with a single scalar key_fields entry naming one input field — it would need either an array-valued key field or a documented convention that safe: false (provider-wide lock) is the only correct declaration for such an operation. This is worth folding into the open question about composite keys already noted in tool/conformance.md, or an explicit note that key_fields as currently specified doesn't cover multi-resource calls and such providers should default conservatively to safe: false.
  2. The best-effort partial-mutation report on cancellation (tool/protocol.md#invoke) is more load-bearing for an apply_patch-style provider than for edit_file. A cancelled single-file edit either applied or didn't; a cancelled multi-file envelope could plausibly have applied some but not all of its file operations before cancellation — exactly the partial-mutation case that section requires a provider to report. A staged-apply model sidesteps this entirely by construction (nothing is mutated until a single atomic apply), which is a point in favor of that pattern for any third-party provider implementing this capability under PluggableHarness Agent, though not something the protocol needs to mandate given it already leaves the mechanism to third parties.

Neither point calls for a protocol amendment on its own — both are observations a third-party multi_file_edit provider author (or a future revision addressing the composite-key open question) should be aware of, not new protocol requirements.