What breaks when you upgrade to OpenCode v2
Understand how OpenCode v2's shared service changes session routing, plugin and client contracts, and the permission behavior you need to check during migration.
Open two OpenCode terminals in different repositories. With the default v2 setup, both connect to one background service. That service owns the sessions, plugins and permissions, so a new terminal window doesn’t give you an independent agent runtime.
This release deep dive compares v1.19.4 with v2.2.5. The migration guide says supported configuration mostly carries over, while plugins and API clients need ports. Some behavior changes without a configuration error: external paths are allowed by default, language servers stop running, and reordered permission rules can change a denial into a prompt.
We’ll follow a session through the shared service, then use that ownership model to explain what needs migration testing. Permission order is the main trap when converting otherwise familiar configuration.
The service routes work by session location
The naive model is that each terminal owns its runtime and its current directory selects the project for every request. That breaks when two terminals share a server. The background server owns application state, so it needs a project context independent of either terminal.
The connection resolver connects to an explicit --server URL or starts a private server for --standalone. Those flags can’t be combined. With the service enabled and neither flag set, it uses Service.ensure to find or start the managed background service.
The session creation code records a location with each session. A new session needs either a location or an existing parent session, whose location it inherits. For session-addressed requests, the location middleware loads the session by sessionID, then provides that location’s services.
Follow the two session IDs below. The diagram starts after session creation and uses shortened IDs to show how one service routes requests for two projects.
Both terminals reach the same service, but each request selects its location through the session. If a client sends the wrong session ID, changing its working directory won’t fix the request’s project context. When debugging an integration, we need to check both its connection and the session it addresses.
Permission order can turn deny into ask
V2 represents permissions as an ordered array of action, resource and effect rules. The migration guide maps the v1 bash permission action to shell. Compare the same policy in the two formats: ask for shell commands generally, but deny commands matching git push *.
{ "$schema": "https://opencode.ai/config.json", "permission": { "bash": { "*": "ask", "git push *": "deny", }, },}{ "$schema": "https://opencode.ai/config.json", "permissions": [ { "action": "shell", "resource": "*", "effect": "ask" }, { "action": "shell", "resource": "git push *", "effect": "deny" }, ],}For git push origin main, both rules match, and the final deny wins. The v2 evaluator flattens the supplied rulesets and uses findLast to select the last matching rule. No match returns ask.
Last-match ordering already existed in v1. Converting to an array creates an opportunity to reorder rules accidentally. Put the same two v2 rules in the opposite order and watch the broad rule override the specific one:
{ "$schema": "https://opencode.ai/config.json", "permissions": [ { "action": "shell", "resource": "git push *", "effect": "deny" }, { "action": "shell", "resource": "*", "effect": "ask" }, ],}Now git push origin main resolves to ask, because the later * matches too. Specificity doesn’t give the deny priority. The separate unattended ruleset settles requests that would otherwise ask, with configured denies checked first.
For admin-enforced restrictions, the migration guide points to policies rather than permissions. When checking ordinary permissions, test a command you expect to deny as well as one you expect to allow.
Extensions need the new service contracts
A plugin runs with the service’s project context, and its implementation needs a port. The plugin migration guide replaces the returned hook object with a default-exported definition containing a stable id and setup(ctx). A returned tool.execute.before hook becomes ctx.tool.hook("execute.before", ...), whose callback receives one mutable event.
Packaging matters too. Configured local paths must name directories, such as ./plugin/local containing index.ts. A configured .ts file path is skipped with a warning, so renaming the configuration key alone can leave a plugin inactive.
Location also needs care inside the port. The guide says ctx.location identifies where the plugin instance loaded. Sessions and events it observes can refer to other locations, so their own data must determine which project they belong to.
Custom interfaces use @opencode/client for the network API. Its event subscriptions are live-only, without replay or automatic reconnection. After a failure, the interface needs to subscribe again and resynchronize its state.
Test behavior before rewriting configuration
We can keep the first migration small because native-format conversion is optional. The migration guide identifies terminal preferences as the other intentional break: supported global settings auto-migrate to cli.json when it’s absent. Project-local terminal settings don’t migrate, and the background service doesn’t load that file.
For an existing setup, this keeps the checks separate:
- Back up the v1 files, then follow the installation and migration guide.
- Keep supported configuration initially. Check credentials, model selection, agents, MCP connections and permission outcomes in a real project.
- Port plugins and API integrations. Check that each plugin actually loads and that clients address the intended session location.
- Replace LSP-dependent feedback with compiler, typecheck or lint commands. V2 preserves
lspconfiguration but doesn’t run language servers or produce their diagnostics. - Convert configuration when useful, then repeat those checks. Keep each agent, provider, command or model entry entirely in one format.
This separates changed behavior from changed syntax. The full guide covers the limits on mixing formats and the field-by-field conversions.
If the terminal connects but work fails, the troubleshooting guide provides two service checks:
opencode service statusopencode api get /api/infoThe first command reports service status. The second checks API reachability and can start the service if needed. A successful API check narrows the problem, after which the session, project and tool workflow still need testing.
For a failure specific to shared-service mode, compare the same workflow with opencode --standalone. That gives you a private server while you investigate. In the normal setup, both terminals from the opening example remain clients of one service, and each session carries the location that tells it where to work.