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.
📚 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 / Function | File | Purpose |
|---|---|---|
FileEditTool | FileEditTool.ts | Main tool definition via buildTool(); name "Edit", strict mode, 100K max result chars |
call() | FileEditTool.ts | Core edit pipeline: read → validate staleness → normalize quotes → patch → write → LSP notify |
validateInput() | FileEditTool.ts | 11 error codes checking permissions, file existence, size, staleness, match uniqueness |
readFileForEdit() | FileEditTool.ts | Reads file with encoding detection (UTF-16LE vs UTF-8) and line ending normalization |
findActualString() | utils.ts | Quote-normalized string matching: tries exact match, then curly-quote normalization fallback |
preserveQuoteStyle() | utils.ts | Transfers curly quote style from actual file content to new_string replacement |
applyEditToFile() | utils.ts | Applies single or all-occurrence replacement; handles trailing newline edge cases |
getPatchForEdit() | utils.ts | Generates structured patch hunks from old_str/new_str; wraps getPatchForEdits() |
getPatchForEdits() | utils.ts | Sequential multi-edit application with substring chaining guard; tab-to-space display conversion |
normalizeFileEditInput() | utils.ts | Normalizes input: strips trailing whitespace, applies desanitization map for API artifacts |
areFileEditsEquivalent() | utils.ts | Semantic edit comparison: fast literal check, then apply-and-compare fallback |
getSnippetForPatch() | utils.ts | Extracts context snippet (4 lines) around changed region for UI display |
getEditToolDescription() | prompt.ts | System prompt with pre-read instruction, line prefix format, replace_all docs |
userFacingName() | UI.tsx | Returns “Create” for new files, “Updated plan” for plans, “Update” otherwise |
Data Structures
| Type | Name | Description |
|---|---|---|
| Input | file_path | Absolute path to the file to modify |
| Input | old_string | Exact text to find and replace in the file |
| Input | new_string | Replacement text (must differ from old_string) |
| Input | replace_all | Boolean (semanticBoolean); replace all occurrences when true |
| Output | FileEditOutput | Contains filePath, structuredPatch, originalFile, gitDiff, replaceAll, userModified |
| Output | StructuredPatchHunk | Diff hunk with oldStart, oldLines, newStart, newLines, and line-level changes |
| Output | gitDiff | Optional: filename, status (modified/added), additions, deletions, patch text |
| Type | FileEdit | Normalized edit: { old_string, new_string, replace_all: boolean } |
| Type | EditInput | Omit<FileEditInput, 'file_path'> — edit without file path context |
| Const | DESANITIZATIONS | 14 mappings for API sanitization reversal (<fnr> → <function_results>, etc.) |
| Const | DIFF_SNIPPET_MAX_BYTES | 8192 bytes cap for attachment snippets |
| Const | CONTEXT_LINES | 4 lines of context before/after change in snippets |
Interactions
| Direction | Partner | Interface | Description |
|---|---|---|---|
| → | LSP server manager | didChange() / didSave() | Post-edit notifications trigger in-memory updates and diagnostics recomputation |
| → | VSCode extension | notifyVscodeFileUpdated() | Notifies IDE for diff view rendering after file modification |
| → | File history | updateFileHistoryState() | Triggers backup before edit if file history feature enabled |
| → | Skills system | dynamicSkillDirTriggers | Fire-and-forget skill activation based on edited file paths |
| → | Git diff | gitDiff.ts | Optional single-file git diff generation for structured output |
| → | Permission system | checkWritePermissionForTool() | Validates file path against allow/deny patterns |
| → | Analytics | logEvent() | Records tengu_write_claudemd and tengu_edit_string_lengths events |
| ← | AgentLoop | tool_use block | Receives edit request from model’s tool call with file_path, old_string, new_string |
| → | FileReadTool | readFileState | Reads 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:
- Pre-critical (async allowed) — Skill discovery, directory creation, file history backup
- Critical section (sync only) — Read file → check staleness → normalize quotes → apply edit → write to disk
- 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 / Parameter | Default | Description |
|---|---|---|
FILE_EDIT_TOOL_NAME | "Edit" | Tool name exposed to the model |
maxResultSizeChars | 100,000 | Maximum result text size before truncation |
strict | true | Enables strict JSON schema validation for input |
MAX_EDIT_FILE_SIZE | 1 GiB | Files larger than this are rejected (ErrorCode 3) to prevent OOM |
DIFF_SNIPPET_MAX_BYTES | 8192 | Cap for diff snippets in attachments and UI display |
CONTEXT_LINES | 4 | Lines 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-gated | Toggles between compact and expanded line prefix format in prompt |
USER_TYPE | unset | When 'ant', enables minimal uniqueness hints in prompt |
replace_all default | false | semanticBoolean: 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.
Staleness Detection Focus
Timestamp-based primary detection, content-based fallback for Windows cloud sync, read-before-write enforcement, and race condition prevention.
Structured Patch Generation Focus
Multi-edit sequential application, substring chaining guards, tab-to-space display conversion, and diff snippet extraction with context lines.
Validation Pipeline Focus
11 error codes from no-op detection to multiple-match resolution, permission layering, Jupyter notebook rejection, and UNC path security.
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 } };
}