old_string drifts from the file by whitespace, indentation, or line endings.
Quick Start
String-name resolution — no import needed
edit_file and apply_patch resolve by name. Enable them via tools=["edit_file", "apply_patch"] in Python, tools: [edit_file, apply_patch] in YAML, or --tools edit_file,apply_patch from the CLI — no explicit import required. Or grab the curated bundle with toolsets=["coding"].How It Works
| Operation | Risk Level | Workspace Required | Purpose |
|---|---|---|---|
| search_files | Low | No | Find patterns in files |
| read_file (edit_tools) | Low | No | Read content + SHA-256 hash; records hash into the shared staleness registry |
| read_file (file_tools) | Low | No | Plain read; also records hash so a later write_file engages the staleness guard |
| list_files | Low | No | Directory listings |
| edit_file | High | Recommended | Precise find-and-replace; automatic per-file lock + staleness guard; force=True to override |
| write_file | High | Recommended | Create/overwrite; automatic per-file lock + staleness guard on existing files; force=True to override |
| apply_patch | High | Recommended | Atomic multi-file Add/Update/Delete with rollback; per-path locks across the whole patch; staleness guard on Update and Delete; force=True to override |
Safe by Default: Concurrency & Staleness
Every read-modify-write on the same file is serialised by a per-file lock, and every write checks whether the file changed since it was last read — both run automatically acrossFileTools, EditTools, and apply_patch.
Per-File Locking
Automatic Staleness Guard
Which Override Do I Need?
Precedence Ladder
| Mechanism | When it fires | How to bypass |
|---|---|---|
Explicit expected_hash | Always when supplied | Pass a matching hash, or omit it and rely on the automatic guard |
| Automatic staleness guard | Only when a prior read_file recorded a hash for the path | force=True (or pass expected_hash explicitly) |
| Per-file lock | Always (cannot be bypassed) | n/a — serialises but never refuses |
expected_hash > force=True > automatic last-read-hash guard.
force=True Examples
How Fuzzy Matching Works
edit_file walks a deterministic ladder of matching strategies and stops at the first strategy that produces a confident match. Exact matches always win; fuzzy strategies only engage when an exact substring is not found. Existing code keeps behaving exactly as before — only previously-failing edits now succeed.
| # | Strategy | Tolerates | Example divergence |
|---|---|---|---|
| 1 | exact | nothing | byte-for-byte match |
| 2 | line_trimmed | leading/trailing whitespace per line | return 1 vs return 1 |
| 3 | whitespace_normalised | collapsed internal whitespace | x = 1 vs x = 1 |
| 4 | indentation_flexible | tabs vs spaces, depth | \treturn 1 vs return 1 |
| 5 | block_anchor | structural drift (similarity ≥ 0.7) | first/last lines anchor a fuzzy block |
- Similarity threshold:
0.7(constant_BLOCK_ANCHOR_THRESHOLDin source) - Disproportionate-length guard: rejects blocks more than 2× or less than half the
old_stringline count - Tie-breaking: two equally-scored candidates are treated as ambiguous (not silently picked)
Why this matters for coding agents: LLM-generated
old_string values routinely drift by whitespace, indentation, or line endings. The fuzzy ladder makes the first attempt succeed when the target is unambiguous, saving retry turns and tokens.Multi-file Patches with apply_patch
apply_patch lets an agent Add, Update, and Delete multiple files in a single atomic call — all changes succeed together or none are committed.
Patch Format Reference
| Header | Body format | Purpose |
|---|---|---|
*** Add File: <path> | Full file content lines until next header | Create a new file (errors if path already exists) |
*** Update File: <path> | One or more @@ hunks (<old>\n===\n<new>) | Modify file using fuzzy ladder for each hunk |
*** Delete File: <path> | (no body) | Remove file (errors if path missing) |
*** Begin Patch / *** End Patch are accepted and stripped.
Update hunk syntax:
@@ hunk runs through the same fuzzy ladder as edit_file, so whitespace/indentation drift in the old block is tolerated.
Atomicity Guarantees
| Behaviour | How it works |
|---|---|
| All-or-nothing | Phase 1 validates every operation and computes new content; Phase 2 commits with staged temp files and os.replace |
| Rollback on failure | If any commit step raises, applied operations are reversed in LIFO order via backup paths |
| BOM preservation | UTF-8 BOM detected on Update is reapplied on write |
| Line-ending preservation | CRLF files stay CRLF, LF files stay LF (matches edit_file) |
| UTF-16 rejection | Update on a UTF-16 file fails with a clear error |
apply_patch Error Messages
| Trigger | Message |
|---|---|
| Empty / no operations | Error: Patch contains no operations |
| Malformed header / orphan body | Error: Invalid patch: Unexpected line in patch (expected a section header): ... |
| Add target already exists | Error: Cannot add '<path>': file already exists |
| Delete target missing | Error: Cannot delete '<path>': file not found |
| Update target missing | Error: Cannot update '<path>': file not found |
| Empty hunk old-block | Error: Empty hunk in update for '<path>' |
| Hunk not found | Error: Hunk not found in '<path>': '<preview>' |
| Ambiguous hunk | Error: Ambiguous hunk in '<path>': '<preview>' matches N locations |
| UTF-16 file | Error: Cannot update '<path>': UTF-16 encoding is not supported. Please convert the file to UTF-8. |
| Automatic staleness — Update | Error: Cannot update '{path}': file changed since it was read - re-read before editing (or pass force=True to override). |
| Automatic staleness — Delete | Error: Cannot delete '{path}': file changed since it was read - re-read before editing (or pass force=True to override). |
| Success | Success: Applied patch to N file(s)\n\n<combined diff> |
Post-edit Diagnostics
After a successful edit,edit_file and apply_patch can run a lightweight checker on the modified file and append concise diagnostics to the tool result — so the agent sees the problem and fixes it in the same loop.
Agent Quick Start
Diagnostics block appears, the agent self-corrects in the next turn — no separate “run the linter” task required.
Three modes
| Mode | When the Diagnostics block appears | Use when |
|---|---|---|
"auto" (default) | Only when the checker reports problems | You want zero noise on clean edits and a self-correction hint on broken ones |
"on" | Always when a checker is available (shows no problems found on clean) | You want positive confirmation in logs/traces |
"off" | Never | You’re in a sandbox with no checkers, or want absolute minimum overhead |
Configure the mode
Which checker runs?
The first installed candidate for the file extension is used. Nothing installed → diagnostics are silently skipped and the tool returns the plain success string.| Extension | Checkers (in order) |
|---|---|
.py, .pyi | ruff → py_compile (stdlib, always available) |
.ts, .tsx, .js, .jsx, .mjs, .cjs | eslint → tsc (TS only) |
.json | stdlib JSON parser |
| anything else | (no checker — silent) |
Output format
Guarantees
- Backward compatible —
automode keeps the success string unchanged when the edit is clean. - Never breaks an edit — a missing, slow, or crashing checker is silently skipped (logged at
DEBUG). - Bounded — 10-second timeout per file; output capped at 2000 characters and truncated cleanly beyond that.
- Sandbox-safe —
eslintruns with--no-config-lookupso workspace-controlled configs/plugins cannot execute code;tscruns per-file without atsconfig.json. - Zero overhead in
offmode — all imports are deferred.
Post-edit Formatting
After a successful edit,EditTools can run a configured language-appropriate formatter on the touched file and persist the formatted result. Default is "off" — byte-for-byte identical to prior behaviour. A missing or failing formatter never turns a successful edit into a failure.
Agent Quick Start
Three modes
| Mode | Behaviour | Use when |
|---|---|---|
"off" (default) | Never runs a formatter | Backward-compatible zero overhead |
"auto" | Run a formatter if one is detected for the file type, otherwise skip silently | Typical projects with formatters installed |
"on" | Same as "auto" — run when detected | Explicit opt-in |
"off".
Built-in formatters
| Extension(s) | Formatter (preference order) | Notes |
|---|---|---|
.py, .pyi | ruff format → black | First on PATH wins |
.ts, .tsx, .js, .jsx, .mjs, .cjs, .json, .css, .scss, .less, .html, .md, .yaml, .yml | prettier | Runs with --no-config --no-editorconfig |
.go | gofmt | |
.rs | rustfmt |
./node_modules/.bin/prettier) resolve relative to the edited file’s directory, not CWD. Formatter runs under a 10-second timeout; timeouts are swallowed. The on-disk hash baseline is only refreshed when the formatter exits 0.
Custom formatter overrides
formatters maps lowercase extensions to (tool_name, argv_template). Use {path} in argv entries; if omitted, the path is appended.
Configuration Options
File Editing Functions
| Function | Args | Returns | Notes |
|---|---|---|---|
edit_file | filepath, old_string, new_string, replace_all=False, expected_hash=None, force=False | str | High-risk; fuzzy ladder + fails on ambiguous match unless replace_all=True; automatic per-file lock + staleness guard |
apply_patch | patch: str, force=False | str | High-risk; atomic multi-file Add/Update/Delete with rollback; per-path locks + staleness guard on Update and Delete |
read_file (edit_tools) | filepath | Tuple[str, str] | (content, sha256_hex); records hash into shared registry |
read_file (file_tools) | filepath, encoding='utf-8' | str | Simple read; also records hash into shared registry |
search_files | directory, pattern, file_pattern='*' | JSON string | Case-insensitive substring search |
write_file | filepath, content, encoding='utf-8', force=False | bool | Full overwrite; automatic per-file lock + staleness guard on existing files |
list_files | directory | list[dict] | Directory listing |
Edit Parameters
| Parameter | Type | Default | Purpose |
|---|---|---|---|
replace_all | bool | False | Required when old_string matches more than once |
expected_hash | Optional[str] | None | SHA-256 hex digest from the last read_file; aborts if the file changed (takes priority over the automatic guard) |
force | bool | False | Bypass the automatic staleness guard for an intentional blind write. An explicit expected_hash is still honoured even when force=True. |
Constructor Parameters (EditTools / create_edit_tools)
| Parameter | Type | Default | Purpose |
|---|---|---|---|
post_edit_diagnostics | str ("auto" | "on" | "off") | "auto" | Run a per-file checker after a successful edit and append concise diagnostics. See Post-edit Diagnostics. Invalid values fall back to "auto". |
post_edit_format | str ("off" | "auto" | "on") | "off" | Run a language-appropriate formatter after a successful edit and persist the result. See Post-edit Formatting. Invalid values fall back to "off". |
formatters | Optional[dict] | None | Override map: extension → (tool_name, argv_template). See Post-edit Formatting. |
Error Messages
| Trigger | Message |
|---|---|
| File not found | Error: File not found: {filepath} |
| UTF-16 encoding | Error: UTF-16 encoding is not supported. Please convert the file to UTF-8. |
Explicit expected_hash mismatch | Error: File has been modified since last read. Please re-read the file before editing. Expected hash: {expected_hash[:8]}..., Current hash: {current_hash[:8]}... |
Automatic staleness — edit_file | Error: File changed since it was read - re-read before editing (or pass force=True to override). Recorded hash: {recorded[:8]}..., Current hash: {current[:8]}... |
Automatic staleness — apply_patch Update | Error: Cannot update '{path}': file changed since it was read - re-read before editing (or pass force=True to override). |
Automatic staleness — apply_patch Delete | Error: Cannot delete '{path}': file changed since it was read - re-read before editing (or pass force=True to override). |
write_file stale refusal | (logger error; tool returns False) Refusing to write {filepath}: file changed since it was read - re-read before writing (or pass force=True) |
write_file verify-read failure | (logger error; tool returns False) Refusing to write {filepath}: could not verify staleness: {err} |
Empty old_string | Error: old_string must be non-empty |
| String not found | Error: String not found in file: '{preview}' |
| Ambiguous match | Error: Ambiguous match - '{preview}' occurs {N} times. Please provide more surrounding context to make the match unique, or use replace_all=True to replace all occurrences. |
| Success | Success: Made {N} replacement(s) in {filepath}\n\nDiff:\n{diff} |
Common Patterns
Code Refactoring
Configuration Updates
Surviving Concurrent Edits
Atomic Multi-file Rename
Best Practices
Read before you edit — the guard is automatic
Read before you edit — the guard is automatic
Any
read_file call (via edit_tools or file_tools) arms the automatic staleness guard for the next write or edit on that path. The guard fires without any extra parameters; just read first and the protection is in place.Use force=True only for intentional blind overwrites
Use force=True only for intentional blind overwrites
force=True bypasses the automatic staleness guard on edit_file, apply_patch, and write_file — but the per-file lock still applies. It’s not a “skip all safety” flag; it only allows the write to proceed when the file changed since the last read. Use it for deliberate rollbacks or idempotent snapshots, not as a routine escape from proper read-before-write discipline.Same path = same lock, different paths = parallel
Same path = same lock, different paths = parallel
The per-file lock is keyed on the canonical path, so two callers that resolve to the same file always serialise — regardless of which module or agent they use. Different files proceed in parallel, so unrelated edits in a multi-agent run are never unnecessarily blocked.
Search Before Edit
Search Before Edit
Use
search_files to locate patterns before editing so you know scope and can craft a unique old_string.Use the returned diff for verification
Use the returned diff for verification
edit_file returns a bounded unified diff (10 lines max, 200 chars per line) so you rarely need a second read.Make old_string unique
Make old_string unique
Include surrounding context so the match is unambiguous; use
replace_all=True only when every occurrence should change.Line endings and BOM are preserved
Line endings and BOM are preserved
CRLF files stay CRLF, LF files stay LF, UTF-8 BOM is preserved. UTF-16 files are rejected with a clear error.
Use apply_patch for multi-file changes
Use apply_patch for multi-file changes
Use
apply_patch when changes span multiple files and must succeed/fail together (rename, refactor, dependency bump). Use edit_file when changing one file in one place — it returns a focused diff and avoids patch syntax overhead.Patch hunk format is not unified diff
Patch hunk format is not unified diff
The patch hunk format uses
@@ to separate hunks and === to separate old from new — this is not unified diff format. Add/Delete sections must not contain @@/=== markers.Workspace Security
Workspace Security
File operations respect workspace boundaries. Paths outside the workspace are rejected to prevent directory traversal.
Post-edit diagnostics mode
Post-edit diagnostics mode
Leave
post_edit_diagnostics at its default ("auto"). When the checker reports problems, the tool result includes a Diagnostics (<tool>): block — instruct the agent to fix the reported issues in its next edit. Switch to "on" if you want positive no problems found confirmations in traces, or "off" if you’re in a sandbox with no checkers available.Related
Workspace
How workspace containment secures file operations — the per-file lock complements workspace boundaries
Bot Default Tools
File tools included in default bot toolsets
Toolsets
Attach the curated
coding toolset to get edit_file + apply_patch + code search + shell + todos in one line
