Description
Describe the bug
edit and apply_patch decode file bytes with a non-fatal UTF-8 decoder (new TextDecoder("utf-8") and new TextDecoder("utf-8", { ignoreBOM: true })). Invalid sequences are silently replaced with U+FFFD.
When oldString matches an ASCII region of a file that contains non-UTF-8 bytes elsewhere (GBK/Latin-1 comments, legacy strings, stray bytes), the tool reports success and writes the entire file back as U+FFFD, permanently destroying every non-UTF-8 byte.
The read tool already rejects this with MalformedUtf8Error (File is not valid UTF-8) via a fatal decoder, but edit/apply_patch don't — so a file that read refuses is silently corrupted by edit.
Steps to reproduce
- Create a file with bytes
const x = 1\nfollowed by GBK bytesd6 d0 ce c4(中文). - edit
oldString: "const x = 1"→newString: "const y = 1". - The file is written back with the GBK bytes replaced by U+FFFD, and the tool reports success.
Affected code
packages/core/src/tool/edit.ts—decodeUtf8usesnew TextDecoder()packages/core/src/tool/apply-patch.ts—new TextDecoder("utf-8", { ignoreBOM: true })
Suggested fix
Decode with { fatal: true } and fail with an explicit "file is not valid UTF-8" error, leaving the file untouched (matching read).
Environment
- opencode version: latest dev
Activity
ekoleszar commentedon Sep 1, 2026
Occurrence: ISO-8859-1 Java project
Same root cause, confirmed in an ISO-8859-1 (Latin-1) legacy Java project where the build compiles with
javac -encoding ISO-8859-1.Observed behavior: the
edittool replaced Spanish non-ASCII bytes (0xF1= ñ,0xE1= á, etc.) with U+FFFD (0xEF 0xBF 0xBD) while editing ASCII regions of the same file. The corruption is silent: no error, and the tool reports success.Why it hurt: because
javac -encoding ISO-8859-1accepts every 0x00-0xFF byte, the corrupted file still compiles — but the .class constant pool now contains mojibake strings (e.g. 'más' instead of 'más'). The bug only shows up at runtime, or when inspecting the binary .class.Environment: OpenCode 1.18.25, Linux, Java source files with Spanish strings encoded in ISO-8859-1.
Recovery used: restored files from git (
git checkout -- <file>) and re-applied the same edits via Python'rb'/'wb'` raw-byte replacement, which preserves encoding byte-for-byte.Suggested scope for the fix: match the
readtool behavior — reject non-UTF-8 files with a fatal decoder beforeedit/apply_patchwrite back, OR preserve detected original encoding on write. At minimum, refuse to write when the file is not valid UTF-8, instead of silently destroying bytes.MasEne1 commentedon Sep 19, 2026
I have a working implementation of the second option mentioned above (preserve the detected original encoding on write), and would like to offer it as a complement to #45925.
#45925 protects the core (v2) tools by rejecting non-UTF-8 files — the right minimal fix there. This covers the v1 tools in
packages/opencode(edit / write / read / apply_patch + the patch engine), which currently still have the same bug, and goes one step further: instead of refusing to touch non-UTF-8 files, it detects the encoding per read (BOM → UTF-8 validity → configurable fallback) and writes back in the same encoding, so legacy files stay editable:file_encoding(default"utf-8"→ default behavior unchanged), e.g."gbk","shift_jis"iconv-lite(zero cost on the UTF-8 path)Happy to scope it down or rebase however maintainers prefer (e.g. land after #45925). Opening a PR referencing this issue.
jlgascon-hiberuscom commentedon Oct 20, 2026
Confirmed on OpenCode 2.0.32 / Windows 11 with Windows-1252 (cp1252) + CRLF Informix 4GL files.
A one-space edit corrupted all 745 existing non-ASCII characters:
Before: 421899 bytes, 0 U+FFFD
After: 423390 bytes, 745 U+FFFD
The +1491 bytes match exactly: 745 × 2 extra bytes from U+FFFD + the single space added.
So this is still reproducible in 2.0.32 with cp1252 files.
opencodejr commentedon Oct 28, 2026
Thanks for the clear report. I reproduced this on 2.2.0 and it still happens on the latest
dev(506ac20).Setup: a file containing
const x = 1\n//followed by the GBK bytesd6 d0 ce c4, and a simulated model.edit(const x = 1→const y = 1): the tool reports success, and the diff shows// ����.od -An -tx1on the file then shows… 2f 2f 20 ef bf bd ef bf bd ef bf bd ef bf bd 0a, so all four GBK bytes were replaced with U+FFFD.patch(offered instead ofeditforgpt-models): the same update corrupts the file the same way on 2.2.0.The code paths in the issue have moved. On current
dev,edit,patchandwriteall read files throughFileMutation.readText, which decodes with a non-fatalTextDecoderinpackages/util/src/bom.ts.The recording and screenshots below show it, and the script below reproduces it.
Related: #49600 and #57308 were closed as duplicates of this issue. #45925 (fatal-decoder fix, closed without merging) and #58113 (open PR adding non-UTF-8 encoding support) are related PRs.
recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.easyteacher commentedon Nov 8, 2026
Additional evidence on 2.2.6 (Windows) —
readis no longer a safe guardRe-confirming the corruption, plus one point that changes the premise of the report: on 2.2.6 the
readtool no longer rejects non-UTF-8 files, so an agent has no way to notice the encoding beforeeditdestroys it.Environment
Windows_NT 10.0.26300C:\WINDOWS\system32\cmd.exe, commands run through PowerShellTest matrix
Files created with .NET
WriteAllText; contentline1 hello 中文测试\r\nline2 end\r\n.readeditwriteEF BB BFFF FECannot read binary fileCould not find oldString, file untouchedd6 d0 ce c4= 中文)1.
readsilently returns U+FFFD instead of failingThe report says
readalready rejects this viaMalformedUtf8Error. That is not what 2.2.6 does:It returns successfully with replacement characters — no
MalformedUtf8Error, no "file is not valid UTF-8". This matters beyond cosmetics: the model sees plausible text, has no signal that the file is non-UTF-8, and proceeds toeditit.2.
editthen destroys the remaining byteseditwitholdString: "line2 end"→newString: "line2 changed"on the same GBK file:The tool reports
Edited ... (1 replacement). All 8 GBK bytes are replaced by U+FFFD.3. UTF-16 with BOM is rejected as "binary", not decoded
A
FF FEBOM file hits the NUL-byte heuristic and returnsCannot read binary file, so BOM-based encoding detection is not active for UTF-16. Oddly this makes UTF-16 the safe case:editfails withCould not find oldStringand leaves the file byte-identical.Suggestion
Make the failure loud at the first touch, in
readas well asedit/patch/write:read, and fail with an explicitFile is not valid UTF-8(thereadpath currently loses this),edit/patch/write, so a non-UTF-8 file is refused rather than rewritten,edit),encodingoption or detect the BOM for UTF-16.Rejecting is strictly better than the current state, because a model that is told "not UTF-8" will convert the file with a byte-preserving tool, while the current silent U+FFFD replacement is unrecoverable without git.