Skip to content

Instantly share code, notes, and snippets.

@SamSaffron
Created January 3, 2026 03:29
Show Gist options
  • Select an option

  • Save SamSaffron/d8b891a7a9d549bc78f19de2d4b98855 to your computer and use it in GitHub Desktop.

Select an option

Save SamSaffron/d8b891a7a9d549bc78f19de2d4b98855 to your computer and use it in GitHub Desktop.

File Editing Engines Audit: Codex vs Gemini-CLI vs OpenCode

This document analyzes how file editing works in three AI coding assistants, highlighting the unique approaches and tricks each uses.


Executive Summary

Feature Codex Gemini-CLI OpenCode
Language Rust TypeScript TypeScript
Edit Tool Name apply_patch replace edit
Write Tool None (use patch) write_file write
Patch Format Custom AI-friendly Literal string replacement Custom patch + literal replacement
Fuzzy Matching 4-tier (exact → unicode normalized) 3-tier (exact → flexible → regex) 9 replacer strategies
Self-Healing No (validation only) LLM-based correction No (falls back through strategies)
Conflict Detection Validation before apply SHA256 content hashing File mtime + lock chains
Path Format Relative only Absolute Absolute
Post-Edit Feedback Success/error only Diff stats LSP diagnostics

1. CODEX (Rust)

Location: codex-rs/apply-patch/

Patch Format

Codex uses a custom declarative patch format designed for AI reliability:

*** Begin Patch
*** Add File: path/to/new.txt
+line1
+line2
*** Update File: path/to/existing.txt
@@ context_marker
 context_line
-old_line
+new_line
*** Delete File: path/to/remove.txt
*** End Patch

Why custom? Traditional unified diffs have ambiguous line numbers that LLMs often get wrong. This format is position-independent—it finds context lines wherever they appear.

Unique Tricks

1. Four-Tier Fuzzy Matching (seek_sequence.rs)

When locating where to apply a patch, Codex tries increasingly lenient matching:

Tier Strategy Handles
1 Exact byte match Perfect input
2 trim_end() Trailing whitespace differences
3 trim() Leading/trailing whitespace
4 Unicode normalization Smart quotes, em-dashes, non-breaking spaces

The Unicode trick is clever—it converts fancy punctuation to ASCII:

fn normalise(s: &str) -> String {
    s.trim().chars().map(|c| match c {
        '\u{2010}'..='\u{2015}' | '\u{2212}' => '-',  // dashes
        '\u{2018}'..='\u{201F}' => '"',               // smart quotes
        '\u{00A0}' | '\u{2002}'..='\u{200A}' => ' ',  // special spaces
        other => other,
    }).collect()
}

This handles copy-pasted code from rich text editors that silently converts " to ".

2. Reverse-Order Application

When a patch has multiple hunks, Codex applies them in reverse line order:

for (start_idx, old_len, new_segment) in replacements.iter().rev() {
    lines.remove(start_idx);
    lines.insert(start_idx, new_line);
}

Why? If you apply from top-to-bottom, each insertion shifts subsequent line numbers. Reverse order means earlier replacements don't invalidate later positions.

3. Context Hints for Large Files

Each hunk can include a change_context line (e.g., def function_name():):

pub struct UpdateFileChunk {
    pub change_context: Option<String>,  // Narrows search to this location
    pub old_lines: Vec<String>,
    pub new_lines: Vec<String>,
}

Instead of searching the entire file, Codex first finds the context marker, then searches nearby. This prevents matching identical lines in the wrong function.

4. Shell Script Extraction

The parser can extract patches from bash heredocs:

cd /project && apply_patch <<'EOF'
*** Begin Patch
...
*** End Patch
EOF

Uses Tree-sitter for robust bash parsing—handles GPT's tendency to wrap patches in shell commands.

Strengths & Weaknesses

Strengths:

  • Very robust against LLM quirks (unicode, whitespace, shell wrapping)
  • Declarative format prevents line-number errors
  • Fast Rust implementation

Weaknesses:

  • No self-correction—fails if context can't be found
  • Non-standard format requires LLM training

2. GEMINI-CLI (TypeScript)

Location: packages/core/src/tools/

Edit Mechanism

Gemini uses literal string replacement as the primary mechanism:

interface EditToolParams {
  file_path: string;
  old_string: string;    // Exact text to find
  new_string: string;    // Replacement text
  expected_replacements?: number;  // Validate occurrence count
}

No patch format—the LLM specifies exactly what to find and replace.

Unique Tricks

1. Three-Tier Replacement Strategy (smart-edit.ts)

If exact match fails, Gemini tries progressively fuzzier matching:

Strategy How It Works
Exact content.indexOf(oldString)
Flexible Line-by-line matching ignoring indent, applies original indentation to replacement
Regex Tokenizes both strings, builds regex with \s* between tokens

The flexible replacer is particularly clever:

// Original file has:    "    const x = 1;"
// LLM sends old_string: "const x = 1;"  (no indent)
// Flexible matcher finds it, preserves "    " prefix in replacement

2. LLM Self-Correction

The killer feature. When edits fail, Gemini asks the LLM to fix them:

// llm-edit-fixer.ts
const prompt = `Your task is to analyze a failed edit attempt and provide
a corrected search string that will match the text in the file precisely.
The correction should be as minimal as possible...`;

const result = await generateJson({
  schema: { search: string, replace: string, noChangesRequired: boolean },
  prompt: `File content:\n${fileContent}\n\nFailed search: "${oldString}"`
});

This handles cases where the LLM hallucinated slightly wrong code—the fixer finds the actual matching text.

Caching: Uses LRU cache (50 entries) to avoid re-calling the LLM for the same failed edit.

3. Safe Dollar-Sign Replacement

JavaScript's string.replace() treats $1, $& etc. as special:

"foo".replace("foo", "$1")  // Throws or produces garbage

Gemini escapes these:

function safeLiteralReplace(str: string, oldString: string, newString: string): string {
  if (!newString.includes('$')) {
    return str.replaceAll(oldString, newString);
  }
  const escapedNewString = newString.replaceAll('$', '$$$$');
  return str.replaceAll(oldString, escapedNewString);
}

Essential for editing JavaScript/TypeScript code with template literals.

4. SHA256-Based Change Detection

Before writing, Gemini checks if the file changed since it was read:

const originalHash = sha256(originalContent);
// ... time passes ...
const currentHash = sha256(fs.readFileSync(filePath));
if (originalHash !== currentHash) {
  throw new Error("File modified externally");
}

Prevents clobbering changes from other editors or parallel LLM requests.

Strengths & Weaknesses

Strengths:

  • Self-healing via LLM correction handles hallucinated code
  • Simple find/replace is easy for LLMs to generate
  • Good caching reduces redundant LLM calls

Weaknesses:

  • No native multi-hunk support (each edit is independent)
  • Self-correction adds latency (40s timeout)
  • Requires LLM call even for simple whitespace mismatches

3. OPENCODE (TypeScript)

Location: packages/opencode/src/tool/ and packages/opencode/src/patch/

Dual Mechanism

OpenCode supports both literal replacement AND a custom patch format similar to Codex.

Unique Tricks

1. Nine Replacer Strategies

The most comprehensive fuzzy matching of the three:

const replacers = [
  SimpleReplacer,              // Exact match
  LineTrimmedReplacer,         // Ignore per-line leading whitespace
  BlockAnchorReplacer,         // First/last line anchor + Levenshtein on middle
  WhitespaceNormalizedReplacer, // All whitespace → single space
  IndentationFlexibleReplacer,  // Remove all common indent
  EscapeNormalizedReplacer,     // Handle \\n, \\t escape sequences
  TrimmedBoundaryReplacer,      // Trim outer boundaries
  ContextAwareReplacer,         // 50% similarity threshold on context
  MultiOccurrenceReplacer,      // Replace ALL matches when requested
];

Each yields candidates; first match wins. This covers edge cases the other engines miss.

2. Levenshtein Distance for Similarity

The BlockAnchorReplacer uses edit distance:

function levenshtein(a: string, b: string): number { /* standard impl */ }

const similarity = 1 - (levenshtein(searchMiddle, candidateMiddle) /
                        Math.max(searchMiddle.length, candidateMiddle.length));

if (candidates.length === 1) {
  threshold = 0.0;   // Accept any anchor match
} else {
  threshold = 0.3;   // Require 30% similarity for disambiguation
}

Clever: For single candidates, it's very permissive (trust the anchors). For multiple candidates, it requires similarity to disambiguate.

3. Promise-Chain File Locking

Serializes writes to the same file:

const locks = new Map<string, Promise<void>>();

async function withLock<T>(filepath: string, fn: () => Promise<T>): Promise<T> {
  const currentLock = locks.get(filepath) ?? Promise.resolve();
  const { promise, resolve } = Promise.withResolvers<void>();
  locks.set(filepath, currentLock.then(() => promise));

  await currentLock;  // Wait for previous write
  try {
    return await fn();
  } finally {
    resolve();  // Allow next writer
  }
}

No external lock files—uses JavaScript's event loop to serialize. Elegant for a single-process tool.

4. Modification Time Assertion

Combines with locking for full protection:

const lastReadTime = fileTimeMap.get(filepath);
const currentMtime = (await fs.stat(filepath)).mtime;

if (currentMtime > lastReadTime) {
  throw new Error("File modified since last read. Please re-read file.");
}

Forces the LLM to re-read after external changes, ensuring it works with current content.

5. LSP Integration

After every edit, OpenCode queries the Language Server:

const diagnostics = await lsp.getDiagnostics(filePath);
return {
  ...result,
  errors: diagnostics.slice(0, 20)  // Return up to 20 errors
};

Immediate syntax/type error feedback helps the LLM self-correct in the next turn.

Strengths & Weaknesses

Strengths:

  • Most comprehensive fuzzy matching (9 strategies)
  • Proper file locking for concurrent safety
  • LSP integration catches errors immediately

Weaknesses:

  • No LLM self-correction (relies on strategy fallback)
  • Complex codebase with two parallel edit systems
  • Levenshtein on every potential match could be slow on large files

Comparison: Key Differentiators

Patch Format Philosophy

Engine Approach Trade-off
Codex Custom declarative format Robust but requires format training
Gemini Pure find/replace Simple but no multi-hunk atomicity
OpenCode Both supported Flexible but complex

Error Recovery

Engine Strategy
Codex Fail with clear error message
Gemini LLM self-correction (expensive but works)
OpenCode Fall through 9 strategies (fast but limited)

Concurrency Safety

Engine Mechanism
Codex None (single-threaded assumption)
Gemini SHA256 content hashing
OpenCode Promise chains + mtime checks

Tool Definitions & Schemas

This section details the actual tools exposed to LLMs and their parameters.


CODEX Tool Definition

Tool Name: apply_patch

Type: Freeform tool (not JSON) - the patch is passed as raw text

Codex has no write_file or edit_file tool. All file modifications go through apply_patch.

How it's called:

{
  "command": ["apply_patch", "*** Begin Patch\n*** Update File: path/to/file.py\n@@ def example():\n- pass\n+ return 123\n*** End Patch"]
}

Grammar (Lark format):

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 := "@@" [ context_header ] NEWLINE { HunkLine } [ "*** End of File" NEWLINE ]
HunkLine := (" " | "-" | "+") text NEWLINE

Key System Prompt Instructions:

  • "Use apply_patch tool to edit files (NEVER applypatch or apply-patch)"
  • "Requires 3 lines of context before/after changes"
  • "Files must be relative paths (never absolute)"

Shell Tool (for completeness):

{
  "name": "shell",
  "parameters": {
    "command": { "type": "array", "items": { "type": "string" } },
    "workdir": { "type": "string", "optional": true },
    "timeout_ms": { "type": "number", "optional": true }
  }
}

GEMINI-CLI Tool Definitions

Gemini exposes three separate tools for file operations:

1. read_file

{
  "name": "read_file",
  "description": "Reads and returns the content of a specified file...",
  "parameters": {
    "file_path": { "type": "string", "required": true },
    "offset": { "type": "number", "description": "0-based line to start from" },
    "limit": { "type": "number", "description": "Max lines to read" }
  }
}

2. replace (the edit tool)

{
  "name": "replace",
  "description": "Replaces text within a file. By default, replaces a single occurrence...",
  "parameters": {
    "file_path": {
      "type": "string",
      "required": true,
      "description": "The path to the file to modify."
    },
    "old_string": {
      "type": "string",
      "required": true,
      "description": "The exact literal text to replace. Include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely."
    },
    "new_string": {
      "type": "string",
      "required": true,
      "description": "The exact literal text to replace old_string with."
    },
    "expected_replacements": {
      "type": "number",
      "minimum": 1,
      "description": "Number of replacements expected. Defaults to 1."
    }
  }
}

Key instruction in description: "NEVER escape old_string or new_string, that would break the exact literal text requirement."

3. write_file

{
  "name": "write_file",
  "description": "Writes content to a specified file in the local filesystem.",
  "parameters": {
    "file_path": { "type": "string", "required": true },
    "content": { "type": "string", "required": true }
  }
}

Hidden parameters (not in schema, used internally):

  • modified_by_user: boolean - set if user edited the proposed change
  • ai_proposed_content: string - original AI proposal before user edits

Smart Edit variant (when enabled) adds:

  • instruction: string (required) - semantic description of the change intent

OPENCODE Tool Definitions

OpenCode uses Zod schemas converted to JSON Schema via z.toJSONSchema().

1. read (ReadTool)

z.object({
  filePath: z.string().describe("The absolute path to the file to read"),
  offset: z.number().optional().describe("Line number to start from"),
  limit: z.number().optional().describe("Number of lines to read"),
})

2. edit (EditTool)

z.object({
  filePath: z.string().describe("The absolute path to the file to modify"),
  oldString: z.string().describe("The text to replace"),
  newString: z.string().describe("The text to replace it with (must be different from oldString)"),
  replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"),
})

Tool description (from edit.txt):

  • "Must use Read tool at least once before editing"
  • "Preserves exact indentation from line number prefix"
  • "Edit will FAIL if old_string is not unique—provide more context or use replace_all"

3. write (WriteTool)

z.object({
  filePath: z.string().describe("The absolute path to the file to write (must be absolute)"),
  content: z.string().describe("The content to write to the file"),
})

4. patch (PatchTool) - currently disabled

z.object({
  patchText: z.string().describe("The full patch text that describes all changes"),
})

Uses same format as Codex (*** Begin Patch etc.)


Tool Schema Comparison

Aspect Codex Gemini OpenCode
Edit tool name apply_patch replace edit
Write tool None (use patch) write_file write
Path parameter Embedded in patch file_path filePath
Path format Relative only Absolute Absolute
Multi-occurrence Via multiple hunks expected_replacements replaceAll: true
Context requirement 3 lines in patch "at least 3 lines" in description Implicit via uniqueness
Schema format Lark grammar JSON Schema Zod → JSON Schema

Tool Registration Architecture

Codex: Tools defined in Rust (spec.rs), converted to OpenAI function-calling format

pub fn to_function_tool(&self) -> ChatCompletionTool {
    ChatCompletionTool {
        r#type: ChatCompletionToolType::Function,
        function: FunctionObject {
            name: self.name.clone(),
            description: Some(self.description.clone()),
            parameters: Some(self.parameters.clone()),
            strict: Some(true),
        },
    }
}

Gemini: Tools registered via ToolRegistry, converted to FunctionDeclaration[]

class ToolRegistry {
  getFunctionDeclarations(): FunctionDeclaration[] {
    return this.tools.map(tool => ({
      name: tool.name,
      description: tool.description,
      parameters: tool.parametersJsonSchema
    }));
  }
}

OpenCode: Tools wrapped with Vercel AI SDK's tool() helper

const tools = {};
for (const [id, item] of Object.entries(registry)) {
  tools[id] = tool({
    description: item.description,
    parameters: jsonSchema(z.toJSONSchema(item.parameters)),
    execute: async (params) => item.execute(params),
  });
}

System Prompt Tool Instructions

Codex (from prompt.md):

Use the `apply_patch` tool to edit files. This is the ONLY way to modify files.
- Always use relative paths
- Include 3 lines of context before and after changes
- Use @@ headers with function/class names for disambiguation

Gemini (from prompts.ts):

3. **Implement:** Use the available tools (e.g., 'replace', 'write_file',
   'run_shell_command'...) to act on the plan...
- Use `read_file` to examine current content before editing
- Use `replace` for targeted text replacements with context
- Use `write_file` for creating new files or complete rewrites

OpenCode (from anthropic.txt, beast.txt):

- You must use your Read tool at least once before editing
- The edit will FAIL if old_string is not unique in the file
- ALWAYS prefer editing existing files. NEVER write new files unless required.

Tool Result Formats

What gets returned to the LLM after an edit:

Codex:

Applied patch successfully.
Files modified: path/to/file.py

On failure:

Error applying patch: Could not locate context lines in file.
Expected to find:
  def example():
      pass

Gemini:

{
  "llmContent": "Successfully replaced 1 occurrence in path/to/file.ts",
  "returnDisplay": "<diff view for UI>",
  "diffStats": { "added_lines": 3, "removed_lines": 1 }
}

On failure (before LLM correction):

{
  "error": {
    "type": "EDIT_NO_OCCURRENCE_FOUND",
    "display": "Could not find the specified text in the file.",
    "raw": "old_string not found in file content"
  }
}

OpenCode:

{
  result: "Successfully edited path/to/file.ts",
  diff: "--- a/file.ts\n+++ b/file.ts\n@@ -1,3 +1,4 @@...",
  diagnostics: [
    { line: 15, message: "Property 'foo' does not exist on type 'Bar'" }
  ]
}

The LSP diagnostics in OpenCode's response is a key differentiator—immediate type errors help the LLM self-correct.


Execution Flow Comparison

CODEX:
  LLM generates patch text
  → Parse grammar (Lark)
  → Validate all hunks exist in files
  → Apply in reverse order
  → Return success/failure

GEMINI:
  LLM calls replace(file_path, old_string, new_string)
  → Try exact match
  → Try flexible match (whitespace-tolerant)
  → Try regex match (tokenized)
  → If all fail AND smart-edit enabled:
      → Call LLM to fix old_string
      → Retry with corrected string
  → Return result with diff stats

OPENCODE:
  LLM calls edit(filePath, oldString, newString)
  → Check file was previously read (mtime tracking)
  → Acquire promise-chain lock
  → Try 9 replacer strategies in order
  → Write file
  → Query LSP for diagnostics
  → Return result with errors

Recommendations for term-llm

Based on this audit:

  1. Adopt Gemini's safe dollar-sign replacement—essential for JS/TS editing
  2. Consider OpenCode's mtime checking—prevents overwriting external changes
  3. Codex's unicode normalization is valuable—handles smart quotes from copy-paste
  4. Skip LLM self-correction initially—adds complexity and latency; try fuzzy matching first
  5. Use reverse-order application for any multi-hunk patch support

The ideal hybrid would use:

  • Literal find/replace as primary (like Gemini)
  • 4-5 fuzzy matching tiers (subset of OpenCode's 9)
  • Codex-style unicode normalization as a fallback tier
  • File mtime checking before writes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment