Editing Files Without an Editor
In part one of this series — Die Hände der KI, in German — we argued that a language model is a voice without hands: everything that looks like doing is a tool layer wrapped around it, and that layer is where control lives. This post goes one level deeper, into the mechanics of a single finger. Because even with hands, a model faces a problem that rarely gets talked about:
It has no editor.
No cursor. No selection. No Ctrl+F highlighting the third match. A human edits text by pointing at it. A model cannot point. It can only describe.
So how does an AI actually change a file?
Counting is not describing
Section titled “Counting is not describing”The naive answer is to describe positions: “replace lines 45–52 with this.” It is how humans talk about code, it is how sed works, and it fails for models in two compounding ways.
First, language models are genuinely bad at counting. They do not perceive a file as numbered lines — they see a token stream. Ask a model for the 45th line of a long file and you get an approximation, delivered with confidence.
Second, and more fundamental: positional addresses expire the moment you use them. Every successful edit shifts every line number below it. In a multi-step editing session the model’s map of the file drifts further from reality with each step. And a bare line-number edit fails silently — line 45 always exists, it is just no longer the line you meant. (Classic patch survives drift precisely because its hunks carry quoted context lines: the escape hatch already points at content.)
The research record is blunt about this. A 2026 study of edit formats (To Diff or Not to Diff?) identifies fragile offsets and fragmented hunks as a core reason classic unified diffs are unnatural for models to generate — the @@ -45,7 +45,9 @@ hunk header is a small counting exercise, and models reliably get it wrong. Another paper (Copy-as-Decode) ran the controlled version of the experiment, perturbing otherwise-correct positional edit references by single steps: pooled exact match dropped from 100% to 15.48%.
One line of drift, 85 points gone. Positions are the wrong address space.
The text is the address
Section titled “The text is the address”What models do far better is reproduce text they have just read — quoting is much closer to their native operation than counting is. So the robust design, arrived at independently by most of the tools that expose file editing directly to an LLM, is:
The text to be replaced is the address.
The model sends an exact quote of what should change (old_str) and its replacement (new_str):
{ "old_str": "timeout: 30\nretries: 3", "new_str": "timeout: 60\nretries: 5"}No positions anywhere. The API searches for the quote and requires it to be unique.
The quiet superpower of this scheme is that it is self-validating — in the one dimension an API can actually check: the quoted text either exists uniquely, or nothing happens and the call fails loudly. That failure carries meaning: the model’s picture of the file is stale. Wrong line numbers produce the opposite — an edit that “succeeds” somewhere unintended. That is silent corruption, the worst failure an editing API can have, because in agent workflows nobody is looking at the file afterwards.
Loud failure is a feature. It is the API telling the model: re-read, then try again.
When the quote is not unique
Section titled “When the quote is not unique”The obvious objection: what if timeout: 30 appears four times?
The tempting fix is to teach the API about document structure — “replace in section X”, “inside function Y”. That road leads to a format-aware API for Markdown, another for YAML, another for Go. The better fix is to apply the same principle recursively: scope by quoting, too.
Two generic mechanisms cover it:
Anchors. Optional after and before parameters take quotes that bracket a window; old_str only needs to be unique inside the window. Headings, function signatures, config keys — the API does not need to know they are “structure”. They are just strings that happen to be naturally unique, and the model is well equipped to pick them. The API stays content-agnostic.
The occurrence round-trip. If the model does not scope and the quote is ambiguous, the API rejects the call — with numbered findings:
{ "error": "ambiguous", "matches": [ { "occurrence": 1, "context": "connect:\n timeout: 30" }, { "occurrence": 2, "context": "read:\n timeout: 30" }, { "occurrence": 3, "context": "write:\n timeout: 30" } ], "retry": "resend with occurrence: N, or narrow with after/before"}The model retries with occurrence: 2. Note what did not happen: the model never counted. The index was assigned by the API, is fresh by construction, and is verifiable against the context shown. Counting has been moved from the party that is bad at it to the party that is trivially good at it.
Anchors are not free — the quoted context costs tokens and eats part of what scoping saves. For a small edit in a large file it is still far cheaper than any alternative.
Select and swap
Section titled “Select and swap”Some edits replace a whole block. Reproducing forty lines as old_str is token waste and a fragility source — one transcription slip in line 23 and nothing matches. The editor gesture for this is click, then shift-click. The API equivalent is replace_range: quote the start, quote the end, replace everything between, inclusive:
{ "from_str": "## Deployment", "to_str": "systemctl restart toolmesh", "new_str": "## Deployment\n\nShip it with the new runbook: ..."}The model describes the boundaries instead of reproducing the body. The boundary quotes follow the same rules as any quote — unique in scope, start before end, validated against the same revision. And because the body is unseen, a well-behaved API offers a dry_run that shows what would be replaced, and echoes the actually replaced text back after a committed write.
The file changed under you
Section titled “The file changed under you”Agents do not edit alone. Another agent, a human, a cron job — anything may have touched the file since it was read. An edit based on a stale read is the lost-update problem, and content addressing alone does not fully solve it: the quote might still match even though the surrounding document has moved on.
The fix is thirty years old: optimistic concurrency. Every read returns a revision. Every write carries it back as base_rev. If the file has changed in between, the write is rejected and the model re-reads and re-quotes. Web developers know this as ETag and If-Match.
Almost nothing in this protocol is novel, and that is the point. The only genuinely new decision is what gets addressed — content instead of positions — because the caller is a system that quotes flawlessly and counts badly.
An escalation ladder, not a single tool
Section titled “An escalation ladder, not a single tool”In practice the protocol is a ladder, and the tool description should say so:
- Small correction → bare
str_replace. - Ambiguous match → add
after/before, or take theoccurrencethe error just handed you. - Substantial block rework →
replace_range, orstr_replacewith the whole paragraph as the quote. - Document under one or two thousand tokens → a full rewrite is legitimate and the most robust move of all. It simply does not scale.
The anti-pattern sits at the top rung: the unscoped full rewrite of a large file. It invites truncation laziness (”… rest of file unchanged …”) and drive-by edits in passages nobody meant to touch. Scoped tools exist precisely so the model never has to hold the whole file in its output.
The tools that ship converged on this
Section titled “The tools that ship converged on this”Look at the editing tools that get real production use:
- Anthropic’s public
text_editortool is built onstr_replacewith enforced uniqueness. - Aider standardized on search/replace blocks after extensive benchmarking; its leaderboard scores models on emitting the edit format correctly, not just on solving the task.
- OpenAI’s
apply_patch(the V4A patch format used by Codex) drops line numbers and addresses each hunk by quoted context lines.
The benchmark record agrees. JetBrains Research’s Diff-XYZ compared edit representations head-to-head; search-replace “performs best for larger models across most tasks” — GPT-4.1 applies edits with it at 0.96 exact match.
There is a second school of thought — Cursor’s fast-apply and Morph-style merge models — that accepts lazy, human-style edits from the big model and trains a small specialized model to apply them. Different mechanics, same underlying distrust of raw positional output against a real file.
When Anthropic, OpenAI, Aider and the benchmark record all land on the same core move — address edits by quoted content — that is not fashion. That is the shape of the constraint.
A living proof: the wiki this post was planned in
Section titled “A living proof: the wiki this post was planned in”We did not arrive at any of this in theory. Our team wiki — where the outline for this very article lived before it became a post — is a small internal tool called Tabula: an LLM-native Markdown wiki over a plain git working tree, built to be edited by agents as much as by people.
Its entire write path is the protocol above:
read_pagereturns{content, rev}— the revision is the futurebase_rev.edit_pageisstr_replace: quote, replace, done. An emptyold_stris rejected; inserts reproduce a surrounding anchor instead.- An ambiguous quote returns
422 ambiguouswith numbered findings and retry options —occurrence: Norafter/before. replace_rangecovers block rework;batch_editapplies several edits atomically — all validate or none apply.- Every write requires a change summary and lands as one git commit.
git logis the audit trail.
Agents edit this wiki daily. Ambiguous matches happen — self-similar prose like config tables and status lists triggers them far more often than code does — and they resolve in a single round-trip. What we have not seen once is a silently corrupted page. That trade, loud failure for cheap recovery, is the entire design.
Hands, with rules
Section titled “Hands, with rules”Part one ended with a claim: intelligence will be rented — your hands should be your own. This post is one of those hands up close: not a window with a blinking cursor, but a narrow protocol of quotes, revisions and honest errors.
The narrowness is what makes the hand governable: every edit names exactly the text it touches, declares the revision it was based on, and lands as a commit someone can diff, revert and attribute. In ToolMesh, our self-hosted gateway, an editing backend like this is just another tool behind the usual permission and audit surface — a read-only agent never sees the write tools at all.
An AI’s editor was never going to look like ours. For an unattended caller, it is better: it cannot touch a line without naming it first.
If you have built — or fought — an editing interface for agents, we would genuinely like to compare notes in GitHub Discussions.