tools/FileEditTool/ — Intelligent File Editing Tool

Applies precise edits to source files using old_str/new_str replacement

The primary way the AI modifies code. Validates edits against current file state, handles curly-quote normalization, supports multi-edit batching via replace_all, produces structured diffs, integrates with LSP for live diagnostics, and maintains staleness detection to prevent race conditions from concurrent edits.

6
Files
11
Error Codes
1 GiB
Max File Size
14
Desanitization Maps

📚 What is this & why does it exist?

FileEditTool is the surgical precision editor for Claude Code. Instead of rewriting entire files, it finds the exact passage to change and replaces just that section. This approach minimizes risk — only the targeted text changes, everything else stays intact. The tool enforces a strict read-before-write discipline and detects if the file was modified by another process between reads, preventing accidental overwrites.

💡 Real-world analogy

Imagine a meticulous copy editor working on a manuscript. Before making any change, they photograph the current page (read the file). They find the exact sentence to modify (old_str match), cross it out, and write the replacement (new_str). If someone else edited the same page while they were working, the editor notices the page looks different and asks to re-read it first. They also handle typography carefully — if the manuscript uses fancy curly quotes but the edit instructions use straight quotes, they automatically preserve the original style.

Contents

Architecture

flowchart TD
  A["Model tool_use: Edit"] --> B["validateInput()"]
  B -->|"ErrorCode 0-10"| C["Return validation error"]
  B -->|"valid"| D["checkPermissions()"]
  D -->|"deny"| E["Permission denied"]
  D -->|"allow"| F["call()"]
  F --> G["readFileForEdit()"]
  G --> H{"Staleness check"}
  H -->|"modified"| I["ErrorCode 8: re-read required"]
  H -->|"fresh"| J["findActualString()"]
  J --> K["preserveQuoteStyle()"]
  K --> L["getPatchForEdit()"]
  L --> M["writeTextContent()"]
  M --> N["LSP didChange + didSave"]
  N --> O["Return structuredPatch"]

Edit Validation Error Codes

flowchart LR
  V["validateInput()"] --> E0["0: old_str = new_str"]
  V --> E1["1: Permission denied"]
  V --> E2["2: UNC path"]
  V --> E3["3: File too large"]
  V --> E4["4: File not found"]
  V --> E5["5: File already exists"]
  V --> E6["6: Jupyter notebook"]
  V --> E7["7: File not read yet"]
  V --> E8["8: Modified since read"]
  V --> E9["9: String not found"]
  V --> E10["10: Multiple matches"]
  style E9 fill:#2a1a1a,stroke:#f85149
  style E10 fill:#2a1a1a,stroke:#f85149

Key Classes & Functions

Class / FunctionFilePurpose
FileEditToolFileEditTool.tsMain tool definition via buildTool(); name "Edit", strict mode, 100K max result chars
call()FileEditTool.tsCore edit pipeline: read → validate staleness → normalize quotes → patch → write → LSP notify
validateInput()FileEditTool.ts11 error codes checking permissions, file existence, size, staleness, match uniqueness
readFileForEdit()FileEditTool.tsReads file with encoding detection (UTF-16LE vs UTF-8) and line ending normalization
findActualString()utils.tsQuote-normalized string matching: tries exact match, then curly-quote normalization fallback
preserveQuoteStyle()utils.tsTransfers curly quote style from actual file content to new_string replacement
applyEditToFile()utils.tsApplies single or all-occurrence replacement; handles trailing newline edge cases
getPatchForEdit()utils.tsGenerates structured patch hunks from old_str/new_str; wraps getPatchForEdits()
getPatchForEdits()utils.tsSequential multi-edit application with substring chaining guard; tab-to-space display conversion
normalizeFileEditInput()utils.tsNormalizes input: strips trailing whitespace, applies desanitization map for API artifacts
areFileEditsEquivalent()utils.tsSemantic edit comparison: fast literal check, then apply-and-compare fallback
getSnippetForPatch()utils.tsExtracts context snippet (4 lines) around changed region for UI display
getEditToolDescription()prompt.tsSystem prompt with pre-read instruction, line prefix format, replace_all docs
userFacingName()UI.tsxReturns “Create” for new files, “Updated plan” for plans, “Update” otherwise

Data Structures

TypeNameDescription
Inputfile_pathAbsolute path to the file to modify
Inputold_stringExact text to find and replace in the file
Inputnew_stringReplacement text (must differ from old_string)
Inputreplace_allBoolean (semanticBoolean); replace all occurrences when true
OutputFileEditOutputContains filePath, structuredPatch, originalFile, gitDiff, replaceAll, userModified
OutputStructuredPatchHunkDiff hunk with oldStart, oldLines, newStart, newLines, and line-level changes
OutputgitDiffOptional: filename, status (modified/added), additions, deletions, patch text
TypeFileEditNormalized edit: { old_string, new_string, replace_all: boolean }
TypeEditInputOmit<FileEditInput, 'file_path'> — edit without file path context
ConstDESANITIZATIONS14 mappings for API sanitization reversal (<fnr> → <function_results>, etc.)
ConstDIFF_SNIPPET_MAX_BYTES8192 bytes cap for attachment snippets
ConstCONTEXT_LINES4 lines of context before/after change in snippets

Interactions

DirectionPartnerInterfaceDescription
→LSP server managerdidChange() / didSave()Post-edit notifications trigger in-memory updates and diagnostics recomputation
→VSCode extensionnotifyVscodeFileUpdated()Notifies IDE for diff view rendering after file modification
→File historyupdateFileHistoryState()Triggers backup before edit if file history feature enabled
→Skills systemdynamicSkillDirTriggersFire-and-forget skill activation based on edited file paths
→Git diffgitDiff.tsOptional single-file git diff generation for structured output
→Permission systemcheckWritePermissionForTool()Validates file path against allow/deny patterns
→AnalyticslogEvent()Records tengu_write_claudemd and tengu_edit_string_lengths events
←AgentLooptool_use blockReceives edit request from model’s tool call with file_path, old_string, new_string
→FileReadToolreadFileStateReads file state cache to check if file was previously read (enforces read-before-write)

Atomicity & Critical Section

The edit pipeline is carefully structured to prevent race conditions. The critical section between staleness check and file write must be synchronous with no async yields:

  1. Pre-critical (async allowed) — Skill discovery, directory creation, file history backup
  2. Critical section (sync only) — Read file → check staleness → normalize quotes → apply edit → write to disk
  3. Post-critical (async allowed) — LSP notifications, state updates, analytics logging, git diff

Source comments explicitly warn: “Do not add async operations between the staleness check and the write operation.”

Configuration

Knob / ParameterDefaultDescription
FILE_EDIT_TOOL_NAME"Edit"Tool name exposed to the model
maxResultSizeChars100,000Maximum result text size before truncation
stricttrueEnables strict JSON schema validation for input
MAX_EDIT_FILE_SIZE1 GiBFiles larger than this are rejected (ErrorCode 3) to prevent OOM
DIFF_SNIPPET_MAX_BYTES8192Cap for diff snippets in attachments and UI display
CONTEXT_LINES4Lines of surrounding context in generated snippets
CLAUDE_FOLDER_PERMISSION_PATTERN/.claude/**Pattern for project-level Claude configuration files
GLOBAL_CLAUDE_FOLDER_PERMISSION_PATTERN~/.claude/**Pattern for user-level Claude configuration files
isCompactLinePrefixEnabled()flag-gatedToggles between compact and expanded line prefix format in prompt
USER_TYPEunsetWhen 'ant', enables minimal uniqueness hints in prompt
replace_all defaultfalsesemanticBoolean: defaults to single replacement; must be explicitly true for all

Directory Map

Source tree for FileEditTool
FileEditTool/
├── FileEditTool.ts     ← Main tool definition: call(), validateInput(), readFileForEdit()
├── UI.tsx              ← React components: userFacingName, diff rendering, rejection display
├── constants.ts        ← FILE_EDIT_TOOL_NAME, permission patterns, error messages
├── prompt.ts           ← System prompt: pre-read instruction, line prefix, replace_all docs
├── types.ts            ← Zod schemas: inputSchema, outputSchema, FileEditOutput, hunkSchema
└── utils.ts            ← Core logic: quote normalization, patch generation, edit equivalence

Deep-Dive Highlights

Core mechanisms from FileEditTool have been extracted into dedicated focus pages for in-depth exploration. Each page covers one algorithm, protocol, or subsystem with diagrams, code walks, and configuration details.

Deep-Dive Pages

Quote Normalization Focus

Curly-to-straight quote matching, apostrophe detection in contractions, opening/closing context analysis, and style preservation in replacements.

Extracted topic page

Staleness Detection Focus

Timestamp-based primary detection, content-based fallback for Windows cloud sync, read-before-write enforcement, and race condition prevention.

Extracted topic page

Structured Patch Generation Focus

Multi-edit sequential application, substring chaining guards, tab-to-space display conversion, and diff snippet extraction with context lines.

Extracted topic page

Validation Pipeline Focus

11 error codes from no-op detection to multiple-match resolution, permission layering, Jupyter notebook rejection, and UNC path security.

Extracted topic page

Code Examples

Example: Quote-Aware String Matching

// utils.ts — Find actual string with curly-quote normalization
export function findActualString(
  fileContent: string,
  searchString: string,
): string | null {
  // 1. Try exact match first — most common case
  if (fileContent.includes(searchString)) {
    return searchString;
  }

  // 2. Normalize both sides: curly quotes → straight quotes
  const normalizedContent = normalizeQuotes(fileContent);
  const normalizedSearch = normalizeQuotes(searchString);

  const idx = normalizedContent.indexOf(normalizedSearch);
  if (idx === -1) return null;

  // 3. Extract actual substring from original file,
  //    preserving the curly quotes as they appear on disk
  return fileContent.substring(idx, idx + normalizedSearch.length);
}

// Preserve the file's quote style in the replacement text
export function preserveQuoteStyle(
  oldString: string,
  actualOldString: string,
  newString: string,
): string {
  // Detect which curly quotes the file uses
  // Apply same style to newString, handling:
  // - Opening vs closing context
  // - Apostrophes in contractions (letter + ' + letter)
  // - Single vs double quote types
  return transformedNewString;
}

Example: Atomic Edit Pipeline

// FileEditTool.ts — Core call() edit pipeline (simplified)
async call(input: FileEditInput, context): Promise<{ data: FileEditOutput }> {
  // Pre-edit: activate conditional skills (non-blocking)
  discoverSkillsForPath(input.file_path, context.dynamicSkillDirTriggers);

  // CRITICAL SECTION: no async yields between staleness check and write
  const { content, encoding, lineEndings } = readFileForEdit(input.file_path);

  // Staleness detection: timestamp-based, content fallback
  if (context.userModified(input.file_path)) {
    throw new Error(FILE_UNEXPECTEDLY_MODIFIED_ERROR);
  }

  // Quote normalization: find actual string preserving file's typography
  const actualOld = findActualString(content, input.old_string) ?? input.old_string;
  const actualNew = preserveQuoteStyle(input.old_string, actualOld, input.new_string);

  // Generate structured patch
  const { patch, updatedFile } = getPatchForEdit({
    filePath: input.file_path,
    fileContents: content,
    oldString: actualOld,
    newString: actualNew,
    replaceAll: input.replace_all,
  });

  // Write atomically with original encoding and line endings
  writeTextContent(input.file_path, updatedFile, { encoding, lineEndings });

  // Post-edit: LSP notifications (non-blocking failures)
  await lspManager.didChange(input.file_path, updatedFile);
  await lspManager.didSave(input.file_path);

  return { data: { filePath: input.file_path, structuredPatch: patch,
    originalFile: content, replaceAll: input.replace_all } };
}