You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
apply_patch turns CRLF files into mixed line endings #45926
apply_patch has no CRLF handling. Patch.derive splits the file on \n, so a CRLF file's lines keep their trailing \r. The model supplies LF context lines (it only ever sees LF from read), so exact matching fails and seek falls back to rstrip (trimEnd()), which matches by stripping the \r. The hunk is then spliced back using the model's LF lines, replacing the CRLF line endings only on the touched lines.
Result: a consistently-CRLF file becomes a mixed-ending file after any update.
This is the apply_patch counterpart of the edit mixed-line-ending issue (#45880); the edit fix (#45888) did not cover the patch/apply_patch path.
Suggested fix
Detect the file's dominant line ending and preserve per-line endings when splicing the hunk (or normalize the touched region consistently), mirroring the edit tool.
Taking this — will preserve each line's original ending when splicing hunks in the patch path (LF context from read shouldn't rewrite CRLF lines), with a unit test on the exact derive repro.
It preserves line endings. If it is a new file, platform native line endings are used. It is also available via npm. However, it is still a workaround.
Thanks for the clear report. I reproduced this on 2.2.0, and it still happens on the latest dev (506ac20c).
What I did:
A simulated model read a 4-line CRLF file app.ts.
It used the patch tool to change one line and add one.
It then ran cat -A app.ts.
Every line inside the hunk, including the unchanged context lines, came out LF ($). The untouched last line kept CRLF (^M$), so the file ends up with mixed line endings.
Your unit-level example also behaves as you describe on dev: Patch.derive("f", [{ oldLines: ["foo"], newLines: ["FOO"] }], "foo\r\nbar\r\n") returns "FOO\nbar\r\n".
The recording and screenshots below show the patch and the cat -A output, and the script below reproduces it. On dev the script may exit with a timeout at its final wait, even though the screen shows the same result.
Related: #54605 (the same problem on Windows with v2.0.26, closed as a duplicate of this issue), and the open PRs #56128 and #58324, which both try to fix it.
recording.mp4
Reproduction script (drive.ts)
// Repro for anomalyco/opencode#45926: the patch tool writes LF line endings into a CRLF file.// The simulated model patches one line of a CRLF file, then runs `cat -A app.ts`// (CRLF lines end in "^M$", LF lines in "$") so the result shows on screen.import{Effect,Stream}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"constpatchText=["*** Begin Patch","*** Update File: app.ts","@@"," const a = 1","-const b = 2","+const b = 20","+const extra = 3"," const c = 3","*** End Patch",].join("\n")exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),tui: {recording: true},project: {git: true,files: {"app.ts": "const a = 1\r\nconst b = 2\r\nconst c = 3\r\nconst d = 4\r\n"},},},({ ui, llm })=>Effect.gen(function*(){constresponses=[[Llm.toolCall({index: 0,id: "call_read",name: "read",input: {path: "app.ts"}}),Llm.finish("tool-calls")],[Llm.toolCall({index: 0,id: "call_patch",name: "patch",input: { patchText }}),Llm.finish("tool-calls")],[Llm.toolCall({index: 0,id: "call_cat",name: "shell",input: {command: "cat -A app.ts",description: "Show line endings"}}),Llm.finish("tool-calls"),],[Llm.text("Lines ending in ^M$ are CRLF, lines ending in $ are LF.")],]yield*llm.serve((_request,index)=>Stream.make(...(responses[index]??[Llm.text("end")])))yield*ui.submit("Change b to 20 in app.ts and add a line, then show line endings")yield*ui.waitFor("are CRLF",{timeout: 60000})yield*Effect.sleep(1500)yield*ui.screenshot("after-patch")}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
Activity
github-actions commentedon Aug 28, 2026
This issue might be a duplicate of existing issues. Please check:
apply_patchmesses up line endings on Windows (CRLF → LF introduced on new/edited lines)The root cause described there overlaps significantly with this report. It may be worth coordinating or referencing that issue.
pengzh1 commentedon Aug 28, 2026
Taking this — will preserve each line's original ending when splicing hunks in the patch path (LF context from read shouldn't rewrite CRLF lines), with a unit test on the exact derive repro.
Mac-Moneysac commentedon Oct 22, 2026
Hi, I workarounded it via a plugin which I've written: https://github.com/Mac-Moneysac/opencode-fix-line-endings
It preserves line endings. If it is a new file, platform native line endings are used. It is also available via npm. However, it is still a workaround.
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(506ac20c).What I did:
app.ts.patchtool to change one line and add one.cat -A app.ts.Every line inside the hunk, including the unchanged context lines, came out LF (
$). The untouched last line kept CRLF (^M$), so the file ends up with mixed line endings.Your unit-level example also behaves as you describe on
dev:Patch.derive("f", [{ oldLines: ["foo"], newLines: ["FOO"] }], "foo\r\nbar\r\n")returns"FOO\nbar\r\n".The recording and screenshots below show the patch and the
cat -Aoutput, and the script below reproduces it. Ondevthe script may exit with a timeout at its final wait, even though the screen shows the same result.Related: #54605 (the same problem on Windows with v2.0.26, closed as a duplicate of this issue), and the open PRs #56128 and #58324, which both try to fix it.
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.