An OpenCode plugin that keeps line endings consistent when the agent writes or patches files:
- Existing files keep the line endings they already have.
- New files get the operating system's native line endings (
os.EOL— LF on Linux/macOS, CRLF on Windows). - Mixed line endings produced by a patch (e.g. LF lines inserted into a CRLF file) are normalized back to the file's dominant ending.
Zero configuration. No runtime dependencies.
Requires OpenCode v2. V1 uses a different plugin API and is not supported by this version — pin
0.0.1if you still need V1.
OpenCode's built-in file tools handle line endings inconsistently:
| Tool | Behavior |
|---|---|
write |
❌ Writes content verbatim (only a BOM is preserved). New files stay LF-only on Windows. |
edit |
\r\n if any CRLF is present, otherwise \n). This covers updates to consistent files, but a pre-existing mixed file stays mixed (untouched lines keep their ending). |
patch |
❌ Strips every trailing CR from the patch text and joins content with \n, so inserted lines are always LF and Add File is LF-only. Updates to CRLF files end up mixed (untouched lines keep \r\n, inserted lines get bare \n). |
On Windows this silently breaks files that require CRLF (e.g. .bat scripts) and pollutes diffs with line-ending churn.
The plugin registers two hooks through the V2 plugin API (Plugin.define → setup → ctx.tool.hook):
execute.before(forwrite): before the file is written, the target's existing line ending is detected — while the original is still intact — and thecontentargument is converted to it. If the file doesn't exist yet,os.EOLis used. This alone makeswritecorrect from the first byte, so no after-hook is needed for it.execute.before+execute.after(foreditandpatch): the before-hook resolves each target file the call is about to touch — forpatchby parsing the patch headers (Add File/Update File/Move to) — and records the ending it should end up with (new files →os.EOL, existing files → their current ending, read while still intact), keyed by the tool call id (event.id). The after-hook then normalizes those files. Since both tools run the project formatter synchronously inside their execution, the after-hook is guaranteed to run after the formatter — the fix gets the last word.
write doesn't need step 2 because its content is fixed before the write ever happens. edit and patch do need it: both only convert their own diff/patch text against an existing, already-consistent file — new files and already-mixed files fall through untouched, which is exactly what the after-hook repairs. The before-observation is required because the after state alone cannot recover the original ending (e.g. an update that rewrites every line loses all traces of CRLF).
Binary safety: content containing a NUL byte (\0) is never touched — the same heuristic Git uses to detect binary files.
desiredEnding picks the ending to normalize a file to, based on what's already in it:
| File content | desiredEnding |
Result |
|---|---|---|
| CRLF + bare CR | \r\n |
→ CRLF |
| pure CR (classic Mac) | \r |
unchanged |
| LF + bare CR | \n |
→ LF |
| CRLF + LF | \r\n |
→ CRLF |
| LF only | \n |
unchanged |
| CRLF only | \r\n |
unchanged |
| no newlines | os.EOL |
no-op |
Bare CR is preserved only in pure-CR files; otherwise it is normalized to the file's dominant ending.
Option 1: From npm (recommended)
Add the package name to the plugins list in your opencode.json(c) — global (~/.config/opencode/opencode.json) for every project, or in your project root for just that project:
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["opencode-fix-line-endings"]
}OpenCode installs and loads npm plugins automatically — no manual download or build step needed. Pin a version with "[email protected]" if you want reproducible installs. To pass options, use the object form ({ "package": "opencode-fix-line-endings", "options": {} }).
Option 2: Local file
OpenCode also loads local plugins straight from a plugin directory — no package.json or build step needed:
~/.config/opencode/plugins/— available in every project (global).opencode/plugins/— available only in this project
Clone the repo and copy the plugin file:
git clone https://github.com/Mac-Moneysac/opencode-fix-line-endings.git
cp opencode-fix-line-endings/index.ts ~/.config/opencode/plugins/fix-line-endings.tsOr download the file directly:
curl -o ~/.config/opencode/plugins/fix-line-endings.ts \
https://raw.githubusercontent.com/Mac-Moneysac/opencode-fix-line-endings/main/index.tsSwap ~/.config/opencode/plugins/ for .opencode/plugins/ in your project if you'd rather install it per-project instead of globally.
Restart OpenCode afterwards — plugins are only loaded at startup. Verified against OpenCode v2.0.11; the edit/patch hook ordering (formatter runs inside the tool, execute.after fires afterwards) was confirmed against the V2 runtime.
- Intentionally mixed line endings within a single file are not preserved — every touched file is unified to a single ending. In practice such files are almost always accidents, which is exactly what this plugin is meant to clean up.
- Formatters: for
write,edit, andpatchthe formatter runs inside the tool call, and this plugin's fix is applied deterministically afterwards (before-write forwrite,execute.afterforedit/patch) — there's no race. - Other write paths (
shell, MCP tools, etc.) aren't covered — onlywrite,edit, andpatchgo through these hooks. - Aborted
edit/patchcalls: if the tool errors out, the after-hook still fires withstatus: "error"and the file is left alone; recorded state for a call that never completes is pruned after 5 minutes rather than kept indefinitely. - The plugin adds one extra file read per touched file (plus a write when a fix is needed). Negligible in practice.
This plugin fixes endings at write time, but it only covers agents running through OpenCode. For a tool-agnostic safety net, add explicit rules to .gitattributes:
* text=auto
*.bat text eol=crlf
*.sh text eol=lfand check working-tree endings with git ls-files --eol.
opencode-line-endings— enforces a configured ending (env var →.editorconfig→ default) instead of preserving the existing one. Use that if you want enforce semantics; use this plugin if you want preserve semantics without configuration.
MIT