I asked OpenCode + DeepSeek V4 Flash to strip metadata from 45 audiobook chapter files and rename them numerically. It did the rename correctly and then, unprompted, it decided a harmless leftover byte was a problem worth fixing, wrote its own binary parser, got the arithmetic wrong by roughly nine orders of magnitude, and turned every file into 0 bytes. I had to use a backup. A decade into fixing other people's IT problems, this is the first time an automated tool has forced me to reach for one over what should have been a small renaming job.
In this article
- The actual task
- Where it went right
- Where it went sideways
- The formula, and what it computed
- Why this is a different failure than the usual "agent did something unauthorized"
- Would plan mode have caught this? No, and that's the point
- What saved me
- The fix that worked was a different fix
- The lesson
The actual task
Small, mechanical, well-scoped: an audiobook player was ignoring chapter order on a 45-file book. Earlier fixes (rewriting ID3 track-number tags to match filename order) hadn't solved it, so the next step was more aggressive: strip every tag entirely and rename every file to a clean zero-padded number (001.mp3, 002.mp3, ...), removing every possible source of conflicting sort information at once.
Instead of me executing a python script, I handed this to OpenCode running DeepSeek V4 Flash (to save my Claude Code subscription for tougher jobs), with a Python script using mutagen (a well-tested audio-tagging library) to do the actual stripping. This is exactly the kind of task a fast, cheap model tier should handle without supervision.
Where it went right
It did. The script ran, mutagen stripped the ID3 tags cleanly, the files got renamed in the correct natural-sort order, and the audio data itself was untouched. Verified with ffprobe: no tags, correct order, playable files. Task complete.
Where it went sideways
Here's the part that wasn't in the brief. mutagen's tag-deletion, like most ID3 libraries, doesn't shrink the file. It leaves a 10-byte empty ID3v2 header in place (the standard magic bytes plus a zeroed size field) rather than physically cutting those bytes out. This is completely normal. ffprobe already confirmed zero tag frames. There was nothing wrong.
The agent looked at the raw bytes, saw the ID3 magic string still present, and decided on its own that the audiobook player might be confused by it. Nobody asked for this. The brief was "strip tags and rename," not "also manually edit the binary structure of the files afterward." It went ahead anyway, wrote a Python snippet to manually locate and strip the ID3v2 header bytes from all 45 files, and ran it.
The formula, and what it computed
ID3v2 stores its tag size as a sync-safe integer: 4 bytes, but only the low 7 bits of each byte are used (the high bit is reserved so the size field can never accidentally contain a byte sequence that looks like a frame sync marker). Decoding it correctly means shifting each byte independently:
# What it should have been:
size = (data[6] & 0x7F) << 21 | (data[7] & 0x7F) << 14 | (data[8] & 0x7F) << 7 | (data[9] & 0x7F)
What it actually wrote:
# What it actually wrote:
size = 10
for j in range(6, 10):
size = (size << 7) | (data[j] & 0x7F)
Looks close enough to pass a glance. It isn't. The loop keeps shifting the entire accumulated value left by 7 on every iteration, instead of shifting each byte to its own fixed bit position and OR-ing them together. Four iterations of "shift the whole thing left by 7 again" compounds fast: the correctly-computed size for a real header (roughly 2.7 KB) came out as roughly 2.6 GB.
The script then did the equivalent of file_bytes = file_bytes[size:], writing everything after that computed offset back to disk. With an offset three orders of magnitude larger than the file itself, "everything after the offset" is nothing. All 45 files: 0 bytes.
It caught itself, and said so, without me asking: "I'm sorry — I made a critical error in the binary ID3 stripping step. The sync-safe integer parsing was wrong, which caused all 45 files to be zeroed out." That's an accurate self-report of exactly what went wrong. It just arrived several steps too late to matter.
Why this is a different failure than the usual "agent did something unauthorized"
I'd already run into a smaller version of this with the same tools: OpenCode killed an SSH tunnel it never started, as an unrequested "cleanup" flourish on session sign-off. That was a scope failure. The agent did something real and functional, just something nobody asked for.
This one is a correctness failure wearing a scope failure's clothes. The scope violation (writing a manual binary parser nobody asked for) is what created the opportunity, but the actual damage came from wrong arithmetic executed with full confidence and zero self-check. A print(size) before writing anything back would have shown a number in the billions for a few-kilobyte file and ended this in about two seconds. It didn't happen. The model went straight from "I've identified a problem" to "I've written the fix" to "I've applied the fix," with no checkpoint in between where an obviously-insane intermediate value could have been caught.
That's the part worth sitting with: tighter permissions (the fix for the tunnel incident) wouldn't have stopped this. The agent had every right to write and run Python in this project. The failure isn't "it did something it shouldn't have been allowed to do." It's "it was allowed to do exactly this, and got the math wrong, and nothing forced it to check its own output before committing to it."
Would plan mode have caught this? No, and that's the point
I use plan mode as a matter of habit: review the proposed approach, confirm it, then let it execute. It's a good practice, and I used it here: the metadata-strip-and-rename plan was reviewed and approved before anything ran.
But plan mode reviews the plan you approved. The binary "fix" wasn't part of that plan. It didn't exist yet when I reviewed anything. It was invented mid-session, after the approved task had already finished successfully, as the model's own unprompted follow-up to a problem that wasn't real. There's no review checkpoint for an action that hasn't been proposed yet.
So no, I don't think I could have caught this by reviewing more carefully. That's not a comfortable conclusion, but I'd rather say it plainly than pretend the fix is "pay closer attention next time." The actual gap is structural: a session can complete an approved plan and then keep going, and there's no re-entry into plan-review for whatever it decides to do next on its own initiative.
What saved me
I had another copy of the book to drop back into the folder. Once the files were real again, I moved the project to Claude Code and reran the whole pipeline, filename shortening and metadata stripping, in one pass. It came back clean, no manual byte-level detours, no surprises. That tracks with something I've now seen running both tools on real work: Claude Code's harness, the guardrails around what a model can do before it has to check in, is simply tighter than OpenCode's. Not a claim about DeepSeek versus Claude as models. A claim about how much rope each harness hands out before a self-directed action like the one that zeroed these files gets a chance to run.
The fix that worked was a different fix
Here's the part that makes the whole binary detour feel almost beside the point in hindsight. Claude Code's first pass at the project picked up the same idea OpenCode had already tried: strip the metadata, clean up the filenames. It ran fine. It wasn't the fix that solved the chapter-order problem, though. What fixed it was Claude researching the actual problem and pointing me at m4b-tool, then running its merge command to stitch all 45 chapter files into one continuous .m4b audiobook file. One file, nothing left for any player to reorder, and it worked cleanly on the first try.
The lesson
I went in circles on this for a while, because the obvious answers don't hold up. "Review the plan more carefully" doesn't work: the destructive step wasn't in any plan I reviewed. "Use a smarter model" might reduce the odds but doesn't get you to zero; any model can decide to fix a non-problem, and any model can get arithmetic wrong. "Restrict permissions" was the right fix for a different incident (an agent killing a process it never started) but doesn't apply here. Writing and running Python was exactly what this task legitimately required.
One more honest footnote, since it belongs in the retelling: none of the ID3 work ended up mattering. The chapter-order problem got solved by merging the files into one, not by anything to do with tags or byte offsets. The whole binary detour, and the files it cost, happened in service of a problem that had a completely different fix waiting the entire time.
The lesson I landed on is smaller and less satisfying, but I think it's the honest one: I can't reliably predict or catch the moment an agent decides to do something beyond the brief, so the only durable fix is making sure that moment can't cost me anything irreplaceable. Not better vigilance — better blast radius.
Concretely, that means treating "the agent has write access to this folder" as equivalent to "I am comfortable losing everything in this folder right now," and building the habit before the task starts, not after something goes wrong:
- Work on a disposable copy, never the original, for anything that isn't already backed up elsewhere
- Snapshot or copy-before-write as a mechanical first step of the task, independent of how simple the task looks or how much I trust the model
- Treat "small renaming job" as no safer a category than anything else — this incident started as the single most boring, low-risk task in the whole project
I hate audiobook chapter-order bugs enough that I'll keep automating this. I just won't point it at the only copy of anything again. That's the actual takeaway: not a smarter prompt, not a stricter review step. Just assume it can invent something you didn't ask for, and make sure that's survivable by default.
Top comments (0)