I ran the upgrade path OpenCode 1.19.0 added on Oct 11 — opencode upgrade --major — against a throwaway HOME, with a real v1-era plugin and a real tui.json, and logged every line. One install that timed out at 500s, one that finished, three startups.
The migration guide lists three intentional breaking changes. In practice one of them shouts at you, one happens silently three steps later, and the third never came up at all.
The setup
export HOME=/tmp/oc2test/home npm install -g --prefix /tmp/oc2test/p1 opencode-ai@1.19.0 # v1 baseline, 355 MB Then the files a normal v1 install has lying around: a plugin at ~/.config/opencode/plugin/secret-guard.ts plus a project copy in .opencode/plugin/, a tui.json with a theme and keybind, and a project opencode.json with permission.bash, permission.edit and one MCP server.
v1 resolved both plugin files (opencode debug config lists them with plugin_origins for global and local scope), so the baseline was clean before I touched anything.
Then I ran the upgrade
opencode upgrade --major --method npm Two things I did not expect.
It needed more than 8 minutes. My first attempt (500s timeout) was killed mid-download, with the shared npm cache already at 493 MB. The second run finished from that warm cache: OpenCode 2 installed, exit code 0.
It does not install where v1 lived. My v1 binary was at /tmp/oc2test/p1/bin/opencode. v2 landed in npm's default global prefix as @opencode/cli@2.0.28, and added two symlinks: opencode and opencode2. The v1 package at my custom prefix was untouched, still runs --version → 1.19.0.
The flag's own help text says "upgrade to OpenCode 2 and remove OpenCode 1". With a custom-prefix install, that did not happen. Both are on the machine, both answer to opencode, and PATH order decides which one you get. On a default install there is only one path, which is presumably why the docs say they can't coexist.
Breaking change #1: the plugin API — the loud one
First v2 startup writes this to the log:
level=WARN message="failed to load plugin" target=/tmp/oc2test/home/.config/opencode/plugin/secret-guard.ts cause="Cause([Fail(PluginModule.LoadError: Plugin must export a default definition with an id and an effect or setup function. (cause: SchemaError(Missing key at ["default"])))]" Both copies failed identically — global and project. And note the level: it is a WARN. The server keeps starting, the session works, and your plugin is simply gone. There is no error dialog and no exit code.
opencode plugin list confirms it: the two v1 plugins are listed with an empty ID column. When I dropped a v2-shaped plugin into the same directory —
export default { id: "acme.hello", async setup(ctx) { /* ... */ }, } — it showed up immediately with acme.hello as its id. Same directory, same file type, different module shape. So this is purely about the export:
- v1:
export const SecretGuard: Plugin = async ({...}) => ({ "tool.execute.before": ..., event: ... }) - v2:
export default Plugin.define({ id, setup })(or an object withid+setup/effect)
Two follow-on details worth knowing:
- v2 still discovers the v1-era
plugin/directory. The v2 docs describe discovery under.opencode/plugins/(plural). My singularplugin/folders were scanned anyway (loading plugin id=.../plugin/secret-guard.ts). The location survived the migration; the code did not. - v2 builds its own plugin package for you. On first start it created
~/.config/opencode/package.json(pinning@opencode-ai/plugin), ran an install — 26 entries innode_modules, including@opencode-ai/sdkandeffect— and wrote a.gitignorethat ignoresnode_modules,package.jsonandpackage-lock.json. If you keep that directory in git, expect new files.
Breaking change #2: the CLI config — silent, and later than you think
Docs: layered tui.json(c) files become one global cli.json, "migrated on first start". Here is what actually happened:
-
opencode2 run "hello"→ nocli.json -
opencode2 debug config→ nocli.json - first TUI start (the default command, with a tty) → finally:
level=INFO message="migrated cli config" from=["/tmp/oc2test/home/.config/opencode/tui.json"] to=/tmp/oc2test/home/.config/opencode/cli.json tui.json is still on disk afterwards — nothing deletes it. The generated file also restructures the content: my "theme": "opencode" came out as "theme": {"name": "opencode"}.
So if you upgrade, run a headless command, look for cli.json, and conclude the migration never ran — it just hasn't run yet. It waits for a terminal.
Breaking change #3: the server API — not verified here
Anything talking to the v1 server has to move to @opencode/client. I had no client integration in this test, so I have nothing first-hand to say about it. That is also the finding: if you don't run a server client, this breaking change is invisible to you, and the other two are what you'll actually hit.
What v2 quietly rewrote in my config
Project opencode.json, written for v1:
"permission": { "bash": "ask", "edit": "allow" }, "mcp": { "demo": { "type": "local", "command": ["echo", "hi"], "enabled": false } } Read back by v2:
"permissions": [ { "action": "shell", "resource": "*", "effect": "ask" }, { "action": "edit", "resource": "*", "effect": "allow" } ], "mcp": { "servers": { "demo": { "type": "local", "command": ["echo", "hi"], "disabled": true } } } bash → shell, and MCP moves under mcp.servers with enabled:false → disabled:true. The docs mention this; what they don't say is that the rewrite is in-memory on read, so any script or README asserting the old field names is now describing something that doesn't exist.
What I'd do before running this on a machine I care about
- Copy
~/.config/opencodesomewhere first. It gets new files (package.json, node_modules,.gitignore, cli.json, service.json) on the first two starts. - Port your plugins before upgrading. v2 only warns, so nothing tells you your guardrails are gone except one WARN line you have to go looking for.
- Start the TUI once, then check
cli.json— don't judge the config migration from a headless run. - Check
which -a opencodeafter upgrading. Two installs, one command name. - Budget the download. It's a few hundred MB and, in my case, more than 8 minutes on a warm network.
Verdict
OpenCode 2 is a real rewrite, but the migration is orderly: opencode.json, agents, commands and skills carried over untouched in my test, and the config rewrite is handled for you. The plugin API is the only place where you have to do work — and it's the one place where silence is the default failure mode.
I kept the throwaway HOME around, so the next test is small: a working v2 plugin, or moving a small @opencode/client call from the v1 server API. If either is useful, say so and I'll write it up.
Sources: OpenCode v1.19.0 release notes, the V2 migration guide and plugin docs, the Anomaly 2.0 notes — plus the local logs from this run. Everything not attributed to a source is from that run: commands, exit codes, log lines, generated files.
