How are code edits applied?
Edit formats (diff, search/replace, whole file, patch); how edits are validated (lint, tests, retries); undo or git integration.
Verdict
Aider gives the most reviewable result, with one git commit per edit. OpenCode and Codewhale forgive the most model mistakes, and PI-Desktop is the hardest to apply to stale text. The app builders mostly rewrite whole files.
Edit blocks parsed from text. Aider uses SEARCH/REPLACE with exact matching, because the fuzzy matcher is disabled in code. It lints after each edit and offers /undo for its own commits.
Exact search/replace tools.
- Cline:
editorneeds a unique match, and GPT/Codex models getapply_patchinstead. Git checkpoints are stored under private refs. - Qwen Code: refuses an edit unless the file was read this session and is unchanged on disk.
- DeepSeek-Reasonix:
multi_editapplies a batch atomically. A per-turn rewind store sits outside git and does not cover bash.
Forgiving matchers.
- OpenCode: nine fallback replacers, LSP diagnostics after each write, and snapshots in a separate git directory.
- Codewhale: exact, then indentation-tolerant, then punctuation-tolerant matching, with an
expected_hashguard. A side git repository backs/restore N.
Patch grammars and anchors.
- Codex: its own
*** Begin Patchgrammar and no lint step. The editor found that the git-snapshotundofeature has been removed at this commit and is kept only as a no-op flag, so rollback is your own git. - Open Interpreter:
apply_patchin native mode. Emulated harnesses edit through alias tools such as a Claude-styleEdit. - PI-Desktop: line-range ops guarded by a 4-hex tag of the file. A stale tag fails the edit.
Whole files (app builders).
- bolt.diy: full files in
<boltAction>tags.saveFileHistoryhas no callers, so there is no file history. - Open Lovable: full
<file>blocks. Snippet merges happen only with a Morph API key, andbuild-validator.tsis never called. - VibeSDK: Think’s exact
editon a git-backed workspace. The model choosescommitrestore points, and rollback re-commits old content.
Pick: Aider for every change as a reviewable commit. Pick: OpenCode or Codewhale for weaker models that slip on whitespace. Pick: VibeSDK for an app builder with real restore points.
More projects in this category are being researched.
Per-project answers
anomalyco/opencode
answeredCode Editing
OpenCode provides three editing tools: edit, write, and apply_patch. Which tool is exposed to the model depends on the model ID — apply_patch is shown for GPT models while edit and write are used for others (tool/registry.ts lines 297-301).
Edit tool (tool/edit.ts): Takes filePath, oldString, newString, and optional replaceAll. It implements a sophisticated multi-strategy search/replace with 9 fallback replacers: SimpleReplacer (exact match), LineTrimmedReplacer (trimmed lines), BlockAnchorReplacer (first/last line anchors with Levenshtein similarity), WhitespaceNormalizedReplacer, IndentationFlexibleReplacer, EscapeNormalizedReplacer, TrimmedBoundaryReplacer, ContextAwareReplacer, and MultiOccurrenceReplacer (lines 694-704). Each replacer is tried in order until a unique match is found. The tool computes a unified diff before applying, shows the diff in permission prompts, normalizes line endings, handles BOM, and formats the file after writing. If the edited file has LSP diagnostics errors, they're reported back to the model (lines 196-201). A semaphore-based file lock prevents concurrent edits (lines 35-45).
Write tool (tool/write.ts): Takes filePath and content. Computes a diff against the existing file (or empty if new), shows it in a permission prompt, writes the file with directory creation, formats it, and reports LSP diagnostics (up to 5 files).
Apply Patch tool (tool/apply_patch.ts): Takes a full patchText in a custom patch format. Validates the patch, parses hunks (supports add, update, delete, move), verifies each file path against external directory checks, computes per-file diffs, asks for permission with the total diff, then applies all changes. After applying, runs LSP diagnostics across all touched files (lines 266-293).
Validation comes from LSP diagnostics after each write/edit operation — if LSP errors are found, the output tells the model to fix them. There is no automated test-run or lint-run after editing in the editing tools themselves (that's left to the model's discretion or a subsequent step).
Undo/git integration: The Snapshot service (snapshot/index.ts) tracks file patches via structuredPatch (diff library) keyed by a hash. Before starting LLM stream execution, a snapshot is captured (track(), line 102 in processor.ts). On step finish, the snapshot is compared and a patch part is persisted (processor.ts lines 471-483). The Session.revert field stores snapshot/diff info for undo operations (session.ts lines 209-214, session/revert.ts).
openai/codex
answeredCodex CLI applies code edits through a dedicated apply-patch crate at codex-rs/apply-patch/src/lib.rs:1. The model sends edits as structured function calls containing patch chunks, which are parsed by the parser module into UpdateFileChunk objects (exported from line 27). The core logic in file_update.rs:25 reads the target file, computes replacements against the original content using compute_replacements() (line 60), and produces the new file contents. The patch format supports change-context matching via seek_sequence::seek_sequence() (line 72) which locates context lines before applying edits, allowing robust search/replace operations. The ApplyPatchFileUpdate struct (exported from line 33) tracks what changed. Edits go through the approval pipeline defined in core/src/tools/approvals.rs:67 where tool calls like ExecCommand and ApplyPatchFileUpdate require user approval based on the configured AskForApproval policy. Validation is handled through the Guardian review system: the tool orchestrator in core/src/tools/orchestrator.rs:1 runs approval → sandbox-selection → execution with retry escalation. There is no built-in linter integration; however, edits are recorded as file diffs and tracked via TurnDiffTracker for display. Undo is supported through Git — Codex discovers the project's Git repository and can diff against HEAD or the remote (the GitDiffToRemoteParams schema suggests git-aware diffing). The approval system also caches decisions per session via ApprovalStore (line 42 in sandboxing.rs) so repeated identical edits don't reprompt.
cline/cline
answeredCode edits are applied through three built-in tools: (1) editor — a search/replace tool that finds exact string matches in a file and replaces them (editor.ts), (2) apply_patch — a unified-diff parser that accepts standard patch format and tolerates legacy shell wrappers (apply-patch.ts), and (3) run_commands — shell execution that can invoke arbitrary editors, compilers, or scripts (bash.ts). The editor tool validates that the old string appears exactly once in the target file (unique match requirement), counts occurrences to prevent ambiguous replacements, and computes a line-based diff for display (editor.ts:67-98). The apply-patch tool parses hunk headers, line positions, and context lines; it supports adding, modifying, renaming, and deleting files, with guardrails like restrictToCwd to prevent path-traversal (apply-patch.ts:59-78). Validation is implicit: the tool returns success/failure and any diff output; the agent sees the result and can retry. There is no automatic lint or test-run after editing. Undo/checkpoint is backed by git: the checkpoint hooks use git stash push --include-untracked with a private refs/cline/restore-transactions/ ref, and checkpoint-restore.ts implements full worktree rollback with commit/rollback semantics (checkpoint-restore.ts:50-80). checkpoint-diff.ts compares checkpoints against the current workspace.
git stash create plus an untracked-files parent under refs/cline/checkpoints/<session>/<run>. stash push --include-untracked with refs/cline/restore-transactions/ is the restore rollback. restrictToCwd only restricts relative paths.openinterpreter/openinterpreter
answeredCode edits use the apply_patch system. The model calls apply_patch (freeform format with inline patches in shell commands). ApplyPatchHandler (core/src/tools/handlers/apply_patch.rs:79-81) routes patch commands. The core library (apply-patch/src/lib.rs) implements parse_patch() and StreamingPatchParser for unified diffs, plus maybe_parse_apply_patch() for extracting patches from shell commands using tree-sitter Bash grammar (apply-patch/src/invocation.rs:1-100). ApplyPatchFileChange supports Add, Delete, and Update (unified-diff hunks with optional move_path). These map to protocol FileChange::Add/Delete/Update (core/src/apply_patch.rs:67-84). Patch streaming sends PatchApplyUpdatedEvent deltas as hunks parse (handler lines 121-158). Validation: assess_patch_safety() checks permission profiles and sandbox routes. The ToolOrchestrator (core/src/tools/orchestrator.rs:1-60) sequences approval to sandbox selection to attempt to retry. Edits run inside the environment filesystem with FileSystemSandboxContext enforcement. There is no lint/test validation step or git integration; safety relies on sandboxing and approvals.
Edit that does exact-string replacement in-process (core/src/tools/handlers/harness_aliases.rs handle_edit).Aider-AI/aider
answeredFour core edit formats, selected per-model. The edit format is determined by the model settings (aider/resources/model-settings.yml) and can be overridden via --edit-format. Available formats include:
diff (SEARCH/REPLACE blocks) — the default for most models. Implemented by
EditBlockCoder(aider/coders/editblock_coder.py). The LLM returns blocks with<<<<<<< SEARCH/=======/>>>>>>> REPLACEfences.get_edits()parses them,apply_edits()performs a literal string replacement viado_replace(). If exact match fails, it tries other chat files, then produces aSearchReplaceNoExactMatcherror withfind_similar_lines()suggestions (editblock_coder.py:38-100).whole (full file replacement) —
WholeFileCoder(aider/coders/wholefile_coder.py). The LLM returns the entire file content inside backtick fences.get_edits()parses the path before the opening fence and the content between fences.udiff (unified diff format) —
UnifiedDiffCoder(aider/coders/udiff_coder.py). The LLM returns standard unified diff hunks. Usessearch_and_replace()andflexible_search_and_replace()fromaider/coders/search_replace.py.diff-fenced —
EditBlockFencedCoder, like diff but with language-specific fence markers.
Function/tool-calling variants. WholeFileFunctionCoder and EditBlockFunctionCoder (sibling modules in aider/coders/) define JSON tool schemas (e.g. write_file with path/content) and the LLM calls them as function calls via litellm's tools parameter (models.py:1006-1009). These function-based coders are not in __all__ by default but can be enabled.
Validation and retries. After edits are applied, if auto_lint is enabled, lint_edited() (base_coder.py:1681-1696) runs a linter on every changed file. If lint errors are found, the user is prompted: "Attempt to fix lint errors?" If yes, the errors become a reflected_message and the LLM gets another turn to fix them (base_coder.py:1599-1607). Similarly for test failures (base_coder.py:1616-1623).
Git integration. Before each message, dirty_commit() (base_coder.py:2411-2423) auto-commits any unstaged changes to relevant files so edits can be undone. After successful edits, auto_commit() (base_coder.py:2375-2395) creates an aider commit. /undo (commands.py:553) reverts the last aider commit.
codewhale-hq/Codewhale
answeredCodewhale provides three file-editing mechanisms with gradual complexity, plus git-based undo:
1. write (WriteFileTool, file.rs:1755): Full file replacement. Accepts path, content, and optional expected_hash. Creates parent directories if needed, preserves line-ending style (CRLF/LF) on overwrite, and writes atomically via run_blocking_write_atomic. The optional content-hash guard (verify_expected_hash, file.rs:89-108) refuses the write if the file changed since it was last read, preventing stale-state overwrites.
2. edit (EditFileTool, file.rs:2581): Search-and-replace with fuzzy matching. Accepts path, search, replace, and optional expected_hash. Matching is multi-layered: exact match first, then indentation-tolerant fuzzy match, then typographic-punctuation (smart quotes, em-dashes) normalization. Returns detailed error messages when search/replace are identical (a common model mistake). After the edit a Rust code formatter (normalize_edit, rust_format.rs) is applied, and LSP diagnostics are injected into the result via lsp_diagnostics_for_paths.
3. apply_patch (ApplyPatchTool, apply_patch.rs:1): Unified diff patching supporting multi-hunk patches with fuzzy matching (MAX_FUZZ=50, default fuzz=3). Hunks can be relocated when their stated line numbers are stale (requires ≥4 anchor lines for safety via MIN_ANCHOR_LINES). Preserves CRLF line endings and trailing-newline state from the base file.
Cross-harness parameter compatibility: All file tools translate known spellings from other coding harnesses (old_string/new_string, file_path, filePath, offset/limit for read windows) onto the canonical names via apply_param_aliases (file.rs:228-255). This prevents rejection loops where the model's training data names parameters differently. Unknown parameters are rejected with explicit errors rather than silently dropped.
Validation pipeline: All mutations go through guard_edit (syntax_check.rs) for syntax validation, normalize_edit for code formatting, and the content-hash guard. Auto-Review gates can flag edits for user approval based on file path patterns.
Git/undo: Workspace snapshots are taken into a side git repository (turn.rs:8-16) at pre-turn, pre-tool, and post-tool boundaries. The /restore N command and revert_turn tool consume these snapshots. The git_tool provides direct git operations (commit, push, branch creation).
esengine/DeepSeek-Reasonix
answeredEdits are applied through three compile-time-builtin tools. write_file (internal/tools/builtin/writefile.go:43) replaces an entire file; it detects no-op writes (same content) and reports them, and creates parent directories. edit_file (internal/tools/builtin/editfile.go:30) does a search-and-replace: old_string must match exactly once in the file, new_string replaces it. A fuzzy-match fallback is available (whitespace-tolerant). multi_edit (internal/tools/builtin/multiedit.go:43) applies a batch of edits atomically to one file — each step runs against the result of the previous one in memory, and the file is only rewritten if every edit succeeds. Each edit tool has a Preview method (internal/tools/builtin/preview.go:25,50) that computes the diff without executing; preview_test.go asserts Preview matches Execute exactly. After every write, a bounded "post-write receipt" (post_write_receipt.go:20) is appended to the conversation — it shows the matched and replacement spans (capped at ~2KB total) so the model can confirm what landed. The FileViews mechanism (internal/tools/builtin/fileviews.go:23) tracks SHA-256 hashes of files the agent read or wrote; write_file checks that the file hasn't changed since the agent last saw it, refusing the overwrite (ErrFileChangedSinceSeen) to prevent clobbering simultaneous edits. There is no lint or test validation between edits and the model's reasoning — that is left to the model's own judgement and the subsequent tool round. Git integration is available via platform/gitcmd for commits and status queries, but edits themselves do not auto-commit; the bash tool can be used to run lint/test commands. All write tools pass through confineWrite which checks write roots, session-data guards, and managed config path protections.
firecrawl/open-lovable
answeredCode edits are applied by parsing the LLM's text output for XML tags. The primary format is <file path="...">full file content</file> (app/api/apply-ai-code-stream/route.ts:70-74) — the LLM is instructed to output COMPLETE files, never diffs. Two additional tag types exist: <package>name</package> signals packages to install, and <command>...</command> commands to run. The parser also handles markdown code blocks (route.ts:130-131). A secondary edit format is the Morph Fast Apply block: <edit target_file="..."><instructions>...</instructions><update>snippet</update></edit> (lib/morph-fast-apply.ts:60-76). When MORPH_API_KEY is set, these blocks are sent to the morph-v3-large model via an OpenAI-compatible API to merge the update snippet into the existing file read from the sandbox (morph-fast-apply.ts:180-217). Validation is handled by build-validator.ts:12-80 which fetches the sandbox URL after writing, checks for a default template page, and looks for Vite error overlays. Truncation detection (unmatched braces, unclosed XML tags) can trigger automatic recovery API calls (generate-ai-code-stream/route.ts:1658-1810), though enableTruncationRecovery defaults to false. File-writing uses the sandbox provider's writeFile() method with a fallback to shell echo or Python open() (vercel-provider.ts:124-189, e2b-provider.ts:108-137). There is no git integration, no undo mechanism, and no diff-based edit format.
QwenLM/qwen-code
answeredCode edits are applied through two tools. Edit (packages/core/src/tools/edit.ts) uses a search-and-replace format: the model provides file_path, old_string, and new_string. The core function applyReplacement() (lines 65–85) uses safeLiteralReplace() to perform the text substitution, with a replace_all option for multiple occurrences and support for new‑file creation (empty old_string + isNewFile flag). WriteFile (packages/core/src/tools/write-file.ts) writes an entire file from scratch with writeRuntimeFile(). Both tools require the file to have been read first in the same turn — this is enforced by checkPriorRead from packages/core/src/tools/priorReadEnforcement.ts.
Before writing, captureRuntimeFileVersion() takes a snapshot of the current file content for diff/undo tracking. After writing, createPatchSmart() in packages/core/src/tools/diffOptions.ts computes the diff, getDiffStat() summarizes it, and the CommitAttributionService tracks session edits. There is no automatic lint, test, or retry loop after edits — the model is expected to verify its own changes by running tools like shell or code review. The edit format is always search-and-replace, never unified diff or whole-file patch. Undo is handled through git integration (session commit tracking and CommitAttributionService), not an explicit undo command.
stackblitz-labs/bolt.diy
answeredCode edits use a structured XML artifact format parsed in real-time during streaming. The LLM emits <boltArtifact> containing <boltAction type="file"> tags with filePath attributes and full file content — Bolt does NOT use diffs or search/replace due to WebContainer limitations (the system prompt explicitly states this at prompts.ts line 36). The StreamingMessageParser (app/lib/runtime/message-parser.ts) incrementally parses the stream: when it sees <boltArtifact> it emits an onArtifactOpen callback (line 289), then for each <boltAction type="file"> it extracts the filePath and accumulates content until </boltAction> closes. On close, it calls onActionClose (line 165) which triggers the ActionRunner. The ActionRunner (app/lib/runtime/action-runner.ts) executes each action sequentially via #executeAction (line 150). For file actions, #runFileAction (line 310) uses WebContainer's filesystem: it resolves the relative path, creates parent folders via mkdir -p, and writes the full file content with webcontainer.fs.writeFile. The EnhancedStreamingMessageParser (app/lib/runtime/enhanced-message-parser.ts) adds a fallback: if the LLM output contains no boltArtifact tags at all, it scans markdown code blocks and wraps them into artifacts automatically using file-path heuristics. There is no git integration for undo — file history is saved separately via saveFileHistory (action-runner.ts line 359) which stores previous versions as JSON under .history/. Shell commands (type="shell") are executed via the terminal emulator, and type="start" launches dev servers. Validation includes pre-flight shell command checks (#validateShellCommand at line 577) that verify paths exist before rm/cd/cp/mv operations.
vastsa/PI-Desktop
answeredEdits use a line-anchored, tag-verified contract (ADR 0087). The Edit tool (runtime.ts:3215-3241) takes path, tag (4-hex fingerprint of whole file from the latest Read/Write/Edit/Grep), and ops — operation strings with +-prefixed body rows. Operations are: PUT N.=M: (replace lines N–M), PUT <N: / PUT >N: / PUT >$: (insert before/after/append), CUT N.=M (delete), REM (delete file), MV DEST (rename). Each successful write returns a new tag for the next edit. The legacy mode (old_string/new_string) is also still available (runtime.ts:3230-3241) but the line-anchored format is the primary contract.
Validation and recovery — Edits are validated at the host-core level (crates/host-core/src/tools/mod.rs). Error codes include EDIT_TAG_MISMATCH, EDIT_TAG_UNKNOWN, EDIT_LINES_UNSEEN, EDIT_PARSE_FAILED, EDIT_RANGE_INVALID, EDIT_NO_CHANGE (packages/shared/src/errors.ts:152-163). The RECOVERABLE_MUTATION_ERROR_CODES set (runtime.ts:403-407) defines which errors get one free retry: tag mismatch, unknown tag, and unseen lines. Each error code has specific advice sent in the tool result (mutationTerminationAdvice(), runtime.ts:409-430). After a failed Edit, the guidance says to re-read the live file and regenerate with fresh tag and narrower anchors.
Concurrency and safety — Host-core enforces tool budgets: max 2 in-flight mutations globally, max 1 per session, max 16 total in-flight tools (crates/host-core/src/tool_budget.rs:7-13). PATH_MUTATING_TOOLS in the runtime also uses a PathMutex to prevent concurrent Write/Edit on the same path (runtime.ts:747).
Write tool — Write is simpler: path + content, create or overwrite. No diffing, no line anchoring. Both Write and Edit go through the same host-core mutation path and the same permission framework.
cloudflare/vibesdk
answeredCode edits are applied through two parallel systems depending on the agent behavior:
ThinkAgent (primary): Edits happen via SpaceDO-backed workspace tools registered in ThinkAgent.getTools(). These use @cloudflare/think/tools/workspace tool creators (createReadTool, createWriteTool, createEditTool, createDeleteTool) backed by SpaceWorkspaceOps which route through Durable Object RPC to SpaceDO. The model can write (whole file), edit (search/replace on existing files), or delete. Edits land directly in SpaceDO's git-backed workspace. Think has no separate diff format validation — the edit tool uses Think's built-in search/replace with exact matching. The model is instructed to keep changes minimal and to deploy after editing.
Agentic/Phasic behaviors: These use a more elaborate system. The agent emits structured outputs matching Zod schemas in worker/agents/schemas.ts — FileOutputSchema for whole-file writes, with a format field supporting full_content or unified_diff. Two diff parsers exist in worker/agents/output-formats/diff-formats/: search-replace.ts (a <<<<<<< SEARCH / ======= / >>>>>>> REPLACE format with multiple matching strategies: exact, whitespace-insensitive, indentation-preserving, fuzzy) and udiff.ts (unified diff format with indentation normalization). Both parse the model's text output into structured edits.
Validation: The Agentic/Phasic flow uses the preDeploySafetyGate utility (worker/agents/utils/preDeploySafetyGate.ts) which runs before deployment to check for hardcoded secrets/API keys in generated code. Static analysis is performed during build. After deploy, browser console logs are inspected. The Think flow deploys via deploy_space and checks browser logs via get_browser_console_logs, forming an automatic repair loop.
Undo/git: All edits go through SpaceDO, which is backed by Cloudflare Artifacts for durable git history. Each commit tool call creates a restore point. rollbackToCommit() in ThinkCodingBehavior restores a prior commit and redeploys without rewriting history. There is no per-edit undo — users roll back to a named commit.
← How is repository context gathered and kept within the context window? · How are shell commands and file writes kept safe? →