Anomaly published the technical breakdown of OpenCode 2.0 on 12 October, and it has been near the top of the Hacker News front page since, with more than 800 points. The headline change is architectural: the open-source coding agent now runs as one background service on your machine, and the terminal UI, the desktop app, the web interface and your own scripts all talk to it. Around that sit a rebuilt API, Code Mode switched on for every MCP server, background subagents and a new plugin API.
It is also a major version in the literal sense. Plugins written for 1.x do not load, the server API is new, the permission config has a different shape, and a few things you may rely on, such as LSP diagnostics and a CLAUDE.md fallback, are gone. This article walks through what changes underneath, what will break when you run opencode upgrade --major, and how to rewrite your permissions so the upgrade does not quietly widen what the agent can do.
One background service now sits under every OpenCode client
OpenCode 1.x already had a client/server split, but the server was off by default and mostly used by power users. In 2.0 it starts automatically as a single background process, and every client connects to it (Anomaly). The practical effect is that one session can move between the terminal, the web UI and the desktop app, and that a script on the same machine can drive the agent without starting its own copy (The New Stack).
- Terminal UI → OpenCode service
- Desktop and web apps → OpenCode service
- Your scripts → OpenCode service: Service.ensure()
- OpenCode service → Agents and background subagents
- Agents and background subagents → Built-in tools
- Agents and background subagents → MCP servers via Code Mode: one script, many calls
- Agents and background subagents → Model providers: HTTPS
Scripts reach the service through @opencode/client. Its Node-only Service API finds a healthy registered service, or starts one with the default command opencode serve --service if none is running, and Service.headers() builds the authentication headers for it (client docs). This is the example from the announcement, which opens a session in the current directory and asks for a review:
import { OpenCode } from "@opencode/client"import { Service } from "@opencode/client/service" const endpoint = await Service.ensure()const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint),}) const session = await client.session.create({ location: { directory: process.cwd() },}) await client.session.prompt({ sessionID: session.id, text: "Review the current changes",})If you want OpenCode inside your own process instead, @opencode/sdk embeds it with no HTTP listener at all; Anomaly says it is the same code that powers the CLI and can run in places such as Cloudflare Durable Objects, but also that it is still in development (SDK docs, Anomaly).
The three changes that break a 1.x setup
The migration guide is unusually direct: there are three intentional breaking changes, and anything else that breaks is to be reported as a compatibility bug. They are the plugin API, the server API and its clients, and the terminal client's configuration, which moves from layered tui.json files to a single global ~/.config/opencode/cli.json. That last one is migrated automatically on first start if cli.json does not exist, but only the global file: project-local client config is not migrated.
Your main opencode.json is not on that list, and V2 reads it from the same locations as V1. It normalises old and new field names in memory and does not rewrite the file (migration guide). That is convenient, but it means the file on disk still says permission and bash while the running service thinks in permissions and shell. The renames worth knowing:
| V1 key | V2 key | Note |
|---|---|---|
permission (grouped by tool) | permissions | Ordered array of action, resource, effect |
bash, task, write/patch | shell, subagent, edit | Permission action names |
agent / mode | agents | prompt → system, maxSteps → steps |
mcp | mcp.servers | enabled → disabled (inverted); timeout splits into catalog and execution |
plugin | plugins | Tuples become { package, options } |
provider | providers | npm → package, api → settings.baseURL |
autoupdate | update | true → "auto", false → "disable" |
logLevel | ignored | Use OPENCODE_LOG_LEVEL |
Plugins are the change most likely to stop you. The config entry is the easy part; the guide shows a package-and-options tuple becoming an object:
- "plugin": ["opencode-example-plugin", ["./plugin/local.ts", { "enabled": true }]]+ "plugins": [+ "opencode-example-plugin",+ { "package": "./plugin/local.ts", "options": { "enabled": true } }+ ]The code is the hard part. The guide warns that renaming a file or a config key is not enough: entrypoints, hooks, tools, events and exports all have to be ported (migration guide). A V2 plugin is a default export built with Plugin.define, with an id and a setup(ctx) that runs at load time and may return a cleanup function. Local plugins in .opencode/plugins/ load without being listed (plugin docs). The smallest complete one:
import { Plugin } from "@opencode/plugin" export default Plugin.define({ id: "example", async setup(ctx) { await ctx.storage.set("loaded", true) },})Anomaly rebuilt almost every built-in feature as an internal plugin, more than 80 of them, so the plugin API is now the same surface the product uses (Anomaly). Hooks cover prompt admission, model requests, permission decisions, shell commands before they run, and tools before and after execution (plugin docs).
Rewrite your permissions as an ordered list
V2 replaces V1's grouped permission object with an ordered list of rules. Each rule has an action (the tool permission), a resource (the path, command, URL or ID being checked) and an effect of allow, ask or deny. The last matching rule wins, so broad rules go first and exceptions after. If nothing matches, the answer is ask (permissions docs).
A V1 block like this one:
{ "$schema": "https://opencode.ai/config.json", "permission": { "bash": { "*": "ask", "git status *": "allow", "git push *": "deny" }, "edit": "allow" }}becomes this in native V2 form:
{ "$schema": "https://opencode.ai/config.json", "permissions": [ { "action": "edit", "resource": "*", "effect": "allow" }, { "action": "shell", "resource": "*", "effect": "ask" }, { "action": "shell", "resource": "git status *", "effect": "allow" }, { "action": "shell", "resource": "git diff *", "effect": "allow" }, { "action": "shell", "resource": "git push *", "effect": "deny" } ]}Order matters in a way it did not before: put git push * above the * rule and the catch-all ask overrides your deny. A shell pattern ending in * also matches the bare command, so git status * covers git status, and when one compound command produces several resources, any deny denies the whole thing (permissions docs).
An empty permissions list in V2 is not a locked-down agent. It is an agent allowed to do almost everything.
That is because every agent starts from a base policy whose first rule is { "action": "*", "resource": "*", "effect": "allow" }. The only exceptions in it are ask for directories outside the project and ask for reading *.env and *.env.* files (with *.env.example allowed) (permissions docs). V1's documentation also said most permissions default to allow, but its stated default for .env files was deny, not ask (V1 permissions docs). If you never wrote a permission block, the V2 default is roughly the V1 one, with .env reads now one keypress away.
Two details make the new model safer than it first looks. Approving a prompt with "always" saves a project-scoped allow rule, but saved approvals never override a configured deny. And a separate policies block, applied after rules and saved approvals, can turn an allow or ask into deny but can never grant anything; global and Console-managed policies outrank project config (permissions docs). Team-wide guardrails belong there, not in each repository's rules.
Code Mode is on for every MCP server
In 2.0 the agent can write a short JavaScript program that calls tools, instead of making one tool call per model round trip. It runs in a minimal interpreter from the @opencode/codemode package, which turns MCP servers and OpenAPI endpoints into functions, and every MCP server is opted in by default (Anomaly). The announcement's example files every open GitHub bug as a Linear issue in one go:
const { issues } = await tools.github.list_issues({ owner: "anomalyco", repo: "opencode", labels: ["bug"], state: "OPEN",}) const filed = await Promise.all(issues.map(({ title }) => tools.linear.save_issue({ team: "OC", title }))) return filed.map(issue => issue.identifier)The pitch is fewer round trips and less context spent on tool definitions; Anomaly says you can add a thousand servers "with minimal context bloat" (The New Stack). The cost is that one approval can now fan out into many writes: the script above creates as many Linear issues as there are open bugs. Permissions still apply inside the script. Code Mode itself is the execute action, MCP tools are matched as <server>_<tool>, and nested tools enforce their own rules (permissions docs). So you can leave Code Mode on and still make the write tools ask:
{ "permissions": [ { "action": "linear_save_issue", "resource": "*", "effect": "ask" }, { "action": "github_create_pull_request", "resource": "*", "effect": "ask" } ]}What quietly stops working
Some changes do not fail loudly. They just remove behaviour you may have been depending on:
- No language servers. V2 accepts and keeps your
lspconfig, but does not run language servers, expose LSP tools or produce LSP diagnostics (migration guide). If your agent relied on type errors from the LSP to fix its own mistakes, give it a type-check command instead. - No
CLAUDE.mdfallback. V2 readsAGENTS.mdbut notCLAUDE.md; the guide says to move that guidance into the applicableAGENTS.md(migration guide). - Agents and commands from
mode/still load, but needmode: primaryin their frontmatter; in commands,subtaskbecomessubagent(migration guide). - No automatic updates. Updates are no longer installed automatically (The New Stack), and V1 and V2 share the
opencodecommand, so a package-managed V1 install should be removed before installing V2 (migration guide). - Server API integrations. Anything that called the 1.x HTTP API must move to
@opencode/client; the endpoints and contracts are new (migration guide).
During the public beta, V2 ran as opencode2 beside V1 and was installed only through package managers (a beta preview write-up). The final migration guide describes a single shared command instead, so advice written during the beta about running both side by side may no longer apply.
Upgrade without widening what the agent can do
Anomaly's own suggestion is to point OpenCode at its migration guide and let it migrate your setup (Anomaly). That is reasonable for config. For permissions, do it by hand and read the result, because a wrong rule order is valid JSON that does the opposite of what you meant. The guide's suggested order is to keep your V1 files, start V2 and verify models, credentials, agents, permissions and MCP servers, then port plugins and API integrations, and only then convert config to the native shape (migration guide).
$ cp -r ~/.config/opencode ~/.config/opencode.v1-backup$ opencode upgrade --majorThe one thing to remember: in 2.0 the service, not the window you are typing in, holds the agent's authority. Configure that service as carefully as you would any other process that can run a shell as you.
Discussion
Join the discussion
Sign in with Google, Apple or an email code to comment and reply.