The Patch That Rewrote Bytes It Was Never Asked to Touch

The request is deliberately tiny: rename one variable in a parser file that has been ISO-8859-1 encoded since before you joined the team. The agent reports success, the diff looks like one hunk, and you open the file. Your editor shows é where é used to be, on lines your request never mentioned. git diff --stat says far more lines changed than you asked for. Nothing errored, nothing warned, and the tool’s own verification said the patch persisted. This is the story of two write paths that could lose bytes they were not even touching — and of why the safety net agreed with them.
A write path is only as exact as its read
Here is the part that is easy to miss when you think about file tools: to patch a file, the tool has to read it first, and whatever that read does to the bytes is what gets written back. Two reads on these paths were lossy.
The default patch mode went through the text transport. mode="replace" — the default — and the V4A edit mode for any byte past the first 1,000-byte binary sample decoded content with errors="replace". That is the polite decoder you want for display: any byte UTF-8 cannot decode becomes U+FFFD, the replacement character. For a human reading a log, that is a favour. For a read that feeds a write, it is a silent mutation — every undecodable byte on lines your edit never touched came back out as EF BF BD. A one-line patch on that Latin-1 parser rewrote every 0xE9 in the file.
The V4A read ran a cleanup it no longer needed. The raw read path pushed the whole file through terminal fence-leak cleanup, which strips OSC escape sequences and BEL bytes, and drops any line containing a __HERMES_FENCE_ literal. That wrapper was a workaround for a backend layer that has since been replaced; nothing emits it any more, which means the cleanup could only ever remove bytes that belonged to your file — control characters in a fixture, a capture, a legacy protocol file.
Why nothing caught it
The tool has a verification step. So does the diff you were shown. Both of them re-read the file through the same lossy transport that had just corrupted it. Corrupt bytes decode to the same replacement characters on the way back, the comparison matches, and the tool reports a clean, persisted patch.
That combination — mutate on read, then verify with the same read — is the pattern worth remembering beyond this project. A verification that shares the transform which caused the damage cannot detect it; it can only confirm that the damage is stable. The reported symptom was therefore never an error message. It was a post-mortem: a reviewer noticing mojibake in git diff, or CI failing on a file whose encoding had quietly changed under it.
What the fix does
PR #120515 makes both patch modes read exactly the bytes they write back. The reads are native on a local POSIX host for regular files (a FIFO still goes down the shell path, with its timeout), and base64 over the wire for remote backends. The content then crosses the text layer with surrogateescape, the codec that round-trips any undecodable byte losslessly — it is the exact inverse of the encode write_file already used for this purpose, pinned by a round-trip test.
One detail explains why they did not simply make “not valid UTF-8” a read error: the V4A operations Add File, Delete File and both ends of Move File use a read failure as their existence check. Turning an odd-encoding file into a read error would have made the tool report that a file which plainly exists does not. surrogateescape keeps such files readable and byte-exact.
The sibling bug: writes that emptied the file
The second fix, PR #122012, is worse in the most direct way. On the heredoc-stdin backends — Modal, Daytona and Vercel — write_file and patch did not corrupt the target. They replaced it with an empty file.
The mechanism is a shell detail: the heredoc was bound to only the last command in the wrapper script (trap - EXIT), so the cat > "$tmp" that was supposed to receive the content read nothing at all, and the empty temp file was moved into place. The fix redirects the heredoc to the whole compound command, and adds the missing half so the body arrives byte-exact — without the trailing newline the heredoc adds, so writes verify.
The follow-up commit is a small masterclass in shell portability: the payload is re-emitted using only shell builtins, deliberately avoiding base64/head, because a missing binary would silently hand the command empty stdin again; and || : is needed because with set -e inherited, read’s end-of-file status killed the reader and produced empty stdin, measured on bash 3.2 and 5.2. A bug that empties files at a distance was fixed without adding a single external dependency to the critical path.
What to do about it
- Know the encoding before you patch.
file -I path/to/filetakes a second and tells you whether you are in UTF-8 or in a legacy single-byte encoding. Anything that is not UTF-8 deserves the checks below. - Verify with bytes, not with the tool.
git diff --stat(line counts, not “looks fine”),cmpagainst a known-good copy, or a hash of the region you did not touch. For binary-adjacent files — fixtures, captures, vendor blobs — a hash is the only honest check. - Treat post-edit verification as suspect when it comes from the same tool. If the tool that wrote the file also tells you the file is fine, that is a statement about its own read path, not about your bytes.
- Re-check anything written to a sandbox backend in the last few weeks. A zero-byte file reports as a file; if a config, lockfile or fixture has been mysteriously empty, this is a candidate explanation. Empty is easier to spot than corrupted, but only if you look.
- Both fixes are on
mainonly. They merged 2026-09-25, one day after the v0.21.5 tag, so they are not in that tag (see the v0.21.5 release page for what it does contain). They arrive with the next release or ahermes updatetrackingmain. If you are on the hosted or sandbox paths today, warm the habit of checking file sizes after a write.
Encoding is a recurring theme in this project’s recent fixes — the Windows locale fix writeup covers the other half of it, where the crash came from decoding rather than writing. And the general question “is my data actually intact?” has a longer treatment in the state database corruption notes: same rule, different storage layer.
What to take away
The interesting failure here is not that a file got corrupted. It is that the verification shared the flaw. A check that runs through the same transform, codec or transport as the operation it is checking can only ever confirm consistency — never correctness. When you build a pipeline that reads, transforms and writes data you did not author, keep at least one independent check that never touches the transform: a byte count, a hash, a size, a copy made before the write. In this case that independent check is exactly what the fix made possible again, and it is the reason the next silent corruption of this kind will show up as a diff instead of a surprise.