OpenCode 2 Ran My Old v1 Config Without Touching It. Here's What It Stopped Doing
In September I created a global OpenCode config and really complex configs for a few projects. Two primary agents, four subagents, and 18 more generated by a Python script from an agent plugin living in a git submodule. Permissions tuned agent by agent. A 52 KB opencode.json that finally stopped fighting me. I put the process to use the agents in an AGENTS.md so I would never have to figure it out again.
OpenCode 2.0.0 landed on npm a few weeks later.
So I looked at the official Migrate from V1 guide and expected a long evening. Nearly every config key got renamed, permissions changed shape, and the guide has a really long table. Ugh, seriously. Then I found the sentence that made most of that optional, and I upgraded without converting a thing.
- The Sentence That Made Things Easier
- What I Actually Did
- The Mistake I Found While Checking
- Converting the Global File With OpenCode Itself
- Permissions Went From a Map to an Ordered List
- Compaction Got Smarter
- The Removals
- The Big Change You Can’t See: One Shared Service
- Subagents Run in the Background Now
- Stuff That Got Easier
- So Is It Worth It?
The Sentence That Made Things Easier
V2 reads your existing config from the same locations as v1: system, managed preferences, global, and project. It normalizes the old keys in memory and does not rewrite your file. Converting to the v2 format is optional, and the old and new forms can coexist in the same file if you want that chaos. If both set the same value, “a valid native V2 value takes precedence regardless of JSON key order.”
Also, very important, inside a single agent, provider, command, or model entry, pick only one format. Don’t put prompt and system in the same agent.
In v1, warnings only show up for malformed values, conflicting old and new values, and fields v2 no longer supports at all. No warnings is a good sign, but it also means v2 won’t nag you about anything it is handling without telling you.
What I Actually Did
I backed up my config, just in case. Your config lives in ~/.config/opencode, but every session you have ever run lives in ~/.local/share/opencode/opencode.db, and mine was 516 MB. I quit OpenCode and copied the whole folder first.
Then I ran the curl installer from the docs:
curl -fsSL https://opencode.ai/v2/install | bash
That replaced my v1 binary, which was 1.19.3 at the time. opencode --version said 2.1.1. I opened it in an empty folder so only my global config applied, picked an agent, and gave it a real piece of work I needed done. It finished with no problems and no startup warnings.
I had been paranoid about this for a week. I was way too paranoid.
The Mistake I Found While Checking
Before upgrading I went looking for every opencode.json I had, because I could not remember how many there were. Six config files and three .opencode/ folders, spread over five projects and the global folder:
| Where | Size | Agents |
|---|---|---|
| Global | 5.8 KB | 8 |
| This blog | 55 KB | 25 |
| My Obsidian vault | 53 KB | 25 |
| Project A | 47 KB | 10 |
| Project B | 42 KB | 11 |
| A Python side project | 17 KB | 9 |
Three different shapes, too. Desk and librarian setups with nested permissions, a flat global file, and an older style I had forgotten existed.
Custom agents in every project felt organized at the time. In practice, when a new major version ships, you have six files to worry about instead of one, and when one of them breaks, you are debugging config instead of working. And they didn’t even have to be JSON. Agents can also be single markdown files in .opencode/agents/, which I didn’t know when I built all this. I want my fallback to be the global config, with my usual models already set up, so I can switch to a plain agent and keep going.
So I cut. The side project’s config was generic and the global agents already covered it, so I deleted it. For the two client projects I kept the one agent I actually use, a hands-on solo agent on Kimi K3 with reasoning at max, moved it into the global config, and deleted both local files. I gave up the file guards those configs had, like denying edits to a raw/ folder, because those rules had been locking me in so hard I stopped using the agents. That left the global file, the blog, and the vault.
Converting the Global File With OpenCode Itself
The guide recommends letting OpenCode convert your config, with this prompt:
Migrate my OpenCode configuration, including file-based definitions, from the V1 format to the native V2 format. Preserve its behavior and all unrelated settings.
I ran it from ~/.config/opencode against the global file, which is small and sits in git, so a bad result was one git checkout away. It worked on the first try. This is the global plan agent, trimmed, before and after:
// v1
"agent": {
"plan": {
"mode": "primary",
"model": "openrouter/~moonshotai/kimi-latest",
"options": { "reasoning": { "effort": "max" } },
"permission": {
"write": "allow",
"edit": "allow",
"bash": "allow",
"task": "allow"
}
}
}
// v2
"agents": {
"plan": {
"mode": "primary",
"model": "openrouter/~moonshotai/kimi-latest",
"request": { "body": { "reasoning": { "effort": "max" } } },
"permissions": [
{ "action": "edit", "resource": "*", "effect": "allow" },
{ "action": "shell", "resource": "*", "effect": "allow" },
{ "action": "subagent", "resource": "*", "effect": "allow" }
]
}
}
Things worth noticing in what it did:
writedisappeared. It merged intoedit, so two rules became one.- Reasoning effort moved to
request.body. I was worried about this one, because the guide says the oldreasoningfield is ignored and describes effort as a variant setting. OpenCode chose to keep it as a request body field. - The top-level keys followed the table.
providerbecameproviderswithnpmturning intopackage: "aisdk:@ai-sdk/openai-compatible",autoupdate: falsebecameupdate: "disable", andsmall_modelbecameagents.title.model. - It dropped
compaction.pruneand renamedreservedtobuffer. More on that below. - It left
lspandenabled_providersalone. The first does nothing in v2 anyway, and the second has a different home in v2 that the converter apparently decided not to touch. - It also edited my
AGENTS.md. The roster doc describes the config, so it updated its own description of the permissions to match. I did not ask for that, but whatever, I guess.
Permissions Went From a Map to an Ordered List
In v1, permissions were an object grouped by tool. In v2 they are permissions, an ordered array of rules with an action, a resource, and an effect. Three action names changed:
bashis nowshelltaskis nowsubagentwriteandpatchcollapse intoedit
A per-pattern v1 rule like "bash": { "git push *": "ask" } becomes one array entry per pattern, with the pattern in resource.
The task rename is one that matters for me. Every subagent in my configs has task: deny so a cheap model can’t kick off a fan-out that blows up the session, except the three workflow orchestrators whose job is fanning out. In v2 that is a subagent rule.
In v1 I learned the hard way that a "*": "deny" catch-all on skills doesn’t just block skills. OpenCode filters the skill list in the prompt by permission, so the model never finds out the skill exists, and there’s no error to notice. An ordered list changes how that trap works. The permissions docs say “The last matching rule wins, so the specific exceptions follow the broad rule,” so a catch-all deny placed last would void every allow above it.
The guide itself doesn’t document the skill or question actions, or what webfetch maps to, so those are the rules to test rather than trust.
So I tested the one thing that I know breaks. I asked desk to have librarian write today’s daily note, which already existed. librarian didn’t overwrite it. It sent a message back to desk explaining the conflict, and desk passed that back to me and waited for a decision. That is the behavior I expected, and it survived the upgrade with the old permission format untouched.
The docs don’t say whether v2 still hides denied skills from the model, so I checked. I asked desk, which has no skill rules, to list every skill it could see, then asked librarian, which denies some skills. Eight of the denied skills were on desk’s list and missing from librarian’s. The other two lists matched exactly. So v2 behaves like v1 here: a denied skill disappears from the prompt.
Compaction Got Smarter
V1 compaction was a summarize-and-pray operation. V2 writes a structured summary meant for another agent to pick the work up from: the objective and requirements, decisions, completed and active work, blockers, next moves, and relevant files. Automatic compaction kicks in near the context limit, governed by a buffer setting. And if a provider rejects a request as too long, v2 compacts and retries once. A second rejection comes back as an error.
My config had "compaction": { "auto": true, "prune": true, "reserved": 10000 }. The converter turned that into auto plus "buffer": 10000 and dropped prune, which v2 ignores.
The Removals
You notice these months later that something stopped happening.
V2 removes a safety check. In v1, OpenCode asked before touching files outside your project. In v2, external_directory is allowed by default. If you want the old behavior back, it’s one rule:
{ "action": "external_directory", "resource": "*", "effect": "ask" }
V2 doesn’t discover CLAUDE.md. It reads AGENTS.md only, so if your instructions live in CLAUDE.md, v2 doesn’t see them. I had one project like that and caught it. The fix is to put the real content in AGENTS.md and make CLAUDE.md a one-line file containing @AGENTS.md. Claude Code follows that import, OpenCode reads the real file, and the two can’t drift apart. Claude Code has also started reading AGENTS.md on its own, as a fallback when there’s no CLAUDE.md. But you know what. Anthropic should have done that long ago instead of being assholes about it. So I still do things this way in case they decide to play the asshole card again.
V2 doesn’t run language servers. If you used it and was counting on diagnostics feeding back into the agent, that is a real regression.

V1 plugins don’t run. There’s a new plugin API and a separate porting guide. No compatibility shim.
A pile of fields are just ignored, including logLevel (use OPENCODE_LOG_LEVEL), top-level subagent_depth (now experimental.subagent_depth), and several provider-model fields.
V2 puts a new file into your config folder. After the upgrade I found a new service.json in ~/.config/opencode with a password in it, for the shared service.
The Big Change You Can’t See: One Shared Service
In v1 the TUI was the thing. In v2, OpenCode discovers or starts one shared background server for your user account, and every local client connects to it. The New Stack called it a major overhaul, and that’s not hype.
What that buys you in practice:
- The terminal and desktop clients should be looking at the same sessions. Start something in the terminal, check on it from the desktop app.
- A web UI you can pair to.
opencode pairserves a web interface, and pairing only works while the shared service is running. The changelog mentionsopencode pair --remote, which asks before exposing your server to anything beyond localhost. - A real API. The server API got new contracts and a typed client package. The docs call it an intentional breaking change, so if you built anything against the v1 server, it’s broken.
I seldom opened the desktop app, and after the upgrade it wouldn’t load my configs at all. It was still a v1 build, 1.3.13. One brew upgrade opencode-desktop took it to 2.1.1, and no errors showed up after that.
Then I asked librarian to write a daily note from the terminal, and the session showed up in the desktop app. I could click into it and read what it was doing while it ran. That’s the shared service doing what the docs promised, and it’s the thing GitHub Copilot never managed for me. I’d start something in VS Code and the Copilot app couldn’t see it, which made the app useless to me. I think they fixed that maybe, but I stopped checking.
Subagents Run in the Background Now
In v1, when a primary agent handed work to a subagent, you waited. In v2, subagents run with fresh context in foreground or background child sessions, each with its own configured permissions, and delegated commands run in the background and report back to the parent session.
I built my vault around delegation. desk routes to librarian, researcher, and editor, and the three cw-* workflow orchestrators fan out to reviewer agents. Every one of those hops used to block. Honestly, I haven’t used that machinery in weeks. These days I use one agent which runs on Kimi K3 and does everything itself, because one agent I can watch beats a whole list of them I have to maintain. But if background subagents work the way the docs say, that might bring me back.

They work. I ran it through desk in the vault, on the 25-agent config I never converted, and the subagents ran in the background while the session stayed usable. The same session was visible in the desktop app while it ran.
Stuff That Got Easier
It’s not all renames. A few things got better for my setup specifically:
- Skill files don’t need rewriting. The guide says so explicitly. The preferred layout is
.opencode/skills/<skill-id>/SKILL.md, and if you move one, move the whole directory, not just theSKILL.md. - Skills paths merge.
skills.pathsandskills.urlsbecome oneskillsarray. - Snapshots are on by default, stored in a separate internal git object database that doesn’t create commits or move your branches.
/undorolls back the conversation and restores the affected files. The docs call capture “best effort,” and restoring doesn’t reverse what shell commands did to services, databases, or git state. - Model variants are built into the model string:
provider/model#variant.
Skill discovery was my other worry. v1 found my vault’s skills through a skills/**/SKILL.md glob, and the v2 docs only promise .opencode/skills/. It found them anyway. When I asked desk to list its skills, everything from the vault showed up.
So Is It Worth It?
For a config like mine, the migration turned out to be mostly a non-event. V2 read my old global file as it was, I ran a real task on it without a warning, and converting the file afterward took one prompt and a diff review. The scary rename table is real, but it only matters when you decide to adopt it.
What actually changed under me is smaller: no language servers, no v1 plugins, no CLAUDE.md, a more permissive default for files outside the project, and a new password file in my config folder.
The better lesson is about my own setup. I had built a custom config in every project, and a major version made me pay for that. I’d rather have one global config with the models and agents I use everywhere, and only add a project-level file when a project really needs it. I split my AI coding rig across two tools so I’d never be stuck when one of them changed under me. This time I did not get stuck, and I still lost an evening to worrying about it.
