Build your own Claude Code
Editing with guards·5 min

Why exact-match edits

0 / 1

You've taught the agent to read the world and to create new files. Now comes the tool it reaches for most — and the one with the sharpest edge. str_replace(path, old_str, new_str) finds a chunk of text in a file and swaps it for another. The whole tool lives or dies on one rule: old_str has to match the file exactly, character for character, whitespace and all. Before you write a line of it, you need to understand why it's built this way — because the design is doing safety work that isn't obvious until you've watched a careless editor corrupt a file.

Why not let the model say "change line 12" or "replace the third return"? Because line numbers drift the instant anything above them changes, and "the third return" assumes the model counted right. An exact-match old_str is content-addressed: it names the text by its bytes, not its position. If those exact bytes appear in the file, the edit lands on them no matter where they moved. If they don't, the edit refuses — loudly — instead of guessing.

Exact isn't enough on its own. If old_str appears twice, which one did the model mean? There's no honest answer, so str_replace demands the match be unique: present exactly once. Absent (zero matches) and ambiguous (two or more) are both errors. The model's job is to widen old_str with surrounding context until it pins down exactly one site — and that's only possible if it read the real bytes first.

Read before edit — old_str is built from real bytes
view("auth.ts")          → model now holds the file's real bytes
str_replace("auth.ts",   ← old_str is a verbatim slice of what view returned
  old_str = "const ttl = 60",
  new_str = "const ttl = 3600")

no prior view  →  the model is guessing at bytes it never saw  →  reject

The stale-write trap

Reading before editing closes one hole and opens another. Suppose the agent reads auth.ts, thinks for a few turns, then issues a str_replace. In between, something else changed the file — a formatter ran, a hook rewrote it, you edited it in your editor. The agent's old_str was built from bytes that no longer exist on disk. Apply the edit anyway and you silently overwrite changes the agent never saw.

This is the stale-write trap, and the fix follows straight from the content-addressed idea. When the agent views a file, you record its current version — a hash of its bytes. When a str_replace arrives, you check that the file's bytes still match the version from the last read. If they moved, the anchor is stale: reject the edit and tell the agent to re-read. A file that was never read has no recorded version at all — also a rejection. Same rule, same reason.

Two steps follow. First you'll build str_replace itself: exact, unique replacement, with clear errors for absent and ambiguous old_str. Then you'll add the stale-write guard: track each file's read-version on view, and reject any edit whose file moved since — or was never read at all. By the end, your editor is one a real agent can trust.