OpenCode didn't want an account. It wanted a model name.
Contents
Assumes you've pointed a terminal coding agent at a model you serve yourself.
The warning reached me second-hand, the way the most confident warnings travel. Don't install a fresh OpenCode 2.x if you run your own models, it said: the new version won't talk to a self-hosted model until you activate an account on opencode.ai and pay for a subscription, and it asks for an API key on your very first prompt. A second message came along with it, an AI assistant's explanation, and that one had a mechanism: fresh installs now default to OpenCode's own hosted model, the switch that turns the hosted provider off is fairly new and may not work, and in that case there's a third-party plugin, installed globally with npx.
I keep public configuration templates for six terminal coding agents, two major versions of OpenCode among them, all pointed at models I serve myself. A warning like that is either a bug report against my templates or a rumour, and I don't get to pick which by reading it harder. So I gave it a throwaway home directory, a fake model server and no way out to the internet, and let it repeat itself.
The account part is false. OpenCode 2.2.5 answered from my local model with no opencode.ai account, no API key and no network. The other half is true, and it's the half that bites: if the config names no model, a new session goes to OpenCode's hosted free model, on somebody else's server. The warning has the risk upside down: the trouble isn't that OpenCode keeps your prompt from your model, it's that it can send it to theirs.
The test: a bare config, no account, no key, no network#
OpenCode V2 is the npm package @opencode/cli. I tested 2.2.5, the newest at the time (published 30 October), and 2.2.0 from 27 October. No hardened template, no account, no key: a bare config with one OpenAI-compatible provider on 127.0.0.1, and headless opencode run. First in a container with no network at all, then with a script that leaves the network on but sends everything non-local to a dead proxy on 127.0.0.1:9, so nothing could reach opencode.ai even if it tried.
The script writes this config three times, into a fresh home directory each time:
cat > "$h/.config/opencode/opencode.json" <<EOF_J
{
"\$schema": "https://opencode.ai/config.json",
"provider": {
"local": {
"npm": "@ai-sdk/openai-compatible",
"name": "local",
"options": { "baseURL": "http://127.0.0.1:$PORT/v1" },
"models": { "qwen-local": { "name": "qwen-local" } }
}
}$model_line
}
EOF_J
$model_line is the only thing that changes. Case A adds "model": "local/qwen-local". Case B adds nothing. Case C adds "disabled_providers": ["opencode"] and still no model. The fake server answers every chat request with the same sentence, LOCAL MODEL ANSWERED, so its words can only come from the server on my machine.
Case B: a model I never configured#
Here is the whole run on 2.2.5:
opencode: opencode v2.2.5
=== case A (with-model)
LOCAL MODEL ANSWERED
=== case B (no-model)
> build · flint-40b-free
Error: Transport: ECONNRESET: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()
=== case C (fix)
LOCAL MODEL ANSWERED
=== requests the local server got (A and C should have some, B none):
4
(temp dir: /tmp/tmp.qySI9NmbUG)
The script sorts and de-duplicates each case's output, so read case B as a set of lines rather than a sequence. Case A answered from the local server. In the no-network container run it also ran a shell tool, echo hello-from-tool, so this isn't a chat that happens to work and an agent that doesn't.
Case B's session line names flint-40b-free: a model I never configured, from a provider I never named, on a server the test couldn't reach. Three nevers in one line of output, delivered as calmly as a menu. That's OpenCode's hosted free model, and the request for it went to opencode.ai and died at my dead proxy. In the container with no network, the same case ended with Error: getaddrinfo EAI_AGAIN opencode.ai instead.
The fake server's log settles who talked to whom: requests came from A and from C, none from B. The script itself only prints the total, 4, for A and C together; it's the log, not that number, that shows B had none. Case B never touched the model I was serving.
Both errors are accurate. The first says a socket closed; the second says a name didn't resolve. Neither mentions the part a user would want to know: that a session they set up for their own server had quietly gone shopping elsewhere.
The hosted model didn't ask me for a key either#
The API key in the warning is the one part I can't reproduce. This test cut case B off before opencode.ai could answer, so on 2.2.5 I never saw what the hosted side says back. I have seen it on 2.2.0. In an earlier test of my templates, V2 picked flint-40b-free with my local fake model up and discovered, and the hosted model answered, with no key asked for. The prompt left the machine.
Where the key prompt came from, I don't know. A first-run screen I didn't test is one candidate, a login on the hosted side another, and I'm not going to pick one in print without having seen it.
Half-true is the most durable kind of claim. The true half survives every check anyone makes of it, and the false half rides along for free. "New sessions go to the hosted model" is checkable and true. "So you need an account and a subscription" is false, and it points the reader at a paywall when the real problem is a leak.
Not new in 2.2.5#
The second message called the default a recent flip. I ran case B on 2.2.0 too, the version my templates were built on, and got the same session line, flint-40b-free. So whatever this is, it isn't new in 2.2.5. I didn't run anything older than 2.2.0, so I can't tell you when it started, only that it predates 2.2.5.
The message also called disabled_providers a recent addition that might not take. The string is in the 2.2.0 binary, and on 2.2.5 it took: case C is the bare config with that one line, and the local model answered.
The fix is two lines of config#
On 2.2.5, with the local server up, either line alone was enough: case A is the first, case C the second.
"model": "local/<your-model-id>", // pick the local model explicitly
"disabled_providers": ["opencode"], // never fall back to the hosted free models
The first pins new sessions to your model. The second removes the hosted provider, so even a session that forgets which model it wants has nowhere else to go. Set both. The deny is the one my templates rely on, and the one my earlier tests checked with no local model answering; the model line alone, with my server down, I haven't tested.
The full script, the steps and what each failure looks like are in the manual Lock OpenCode 2.x to your self-hosted model. The script runs all three cases in a temporary directory and never reads your own ~/.config/opencode.
My templates already said no to that provider#
None of this was news to my templates, which is the embarrassing part. The research behind them found that with no local model answering, OpenCode falls back to the hosted free models, and the prompt and the code go with it, off the machine. V2 2.2.0 without a deny chose the hosted model even with a local one up. So the V1 template sets "disabled_providers": ["opencode"], and the V2 template adds a global policy:
{ "action": "provider.use", "resource": "opencode", "effect": "deny" },
Global policies outrank anything a project's own config says, so a repository can't switch the hosted provider back on behind my back. With the template installed, 2.2.5 passed the same guardrail cases as 2.2.0, three of three in a quick run. The evening I first used those templates for real work turned up six findings; this wasn't one of them.
So I had the answer in writing, in my own config, and I still went and tested a rumour about it. That's not modesty. The one thing I trust less than a forwarded claim is my memory of my own research, and today the two agreed only on the half that was true.
The plugin I didn't install#
opencode-disable-zen exists on npm. It's third-party code, and nothing in these tests needed it: case C did its job with one line of the config OpenCode already reads. I didn't install it and I haven't reviewed it, so I have nothing to say about what it does. I'd only point out the shape of the trade it offers: one config line OpenCode already reads, or a package I haven't read, installed globally, to do the same thing. I'll keep the config line.
What I didn't test#
Only headless opencode run. I didn't open the interactive TUI, and I didn't walk through a fresh install's first-run screens, which is the one place I'd expect a login prompt to look most like a requirement. If the TUI asks for an account before it lets you reach a local model, these tests wouldn't have seen it.
The two messages got the default right and the danger backwards. OpenCode never asked me for an account. It asked which model I meant, and when I didn't say, it answered the question itself, with a model from somebody else's server. Name the model, switch off the provider, and it talks to your machine and nothing else. I've got that in two lines now, and in a script that checks it for me, which I trust more than either message, and a little more than my memory.
Filed under: ai-agents, opencode, local-llm, security
Read next#
- My agent guardrails passed every test. Then I used them.: the templates these tests sit next to, and six things a real evening turned up.
- Lock OpenCode 2.x to your self-hosted model: the complete script and the steps, to check your own install.
- The model that only says RUN: the scripted fake model behind my agent tests, a bigger sibling of the one-sentence server here.
- My agents got their own config. Two still read mine.: where I first caught OpenCode sending a test prompt to its hosted model, and why the V2 deny has to live under
policies.
Sources#
- Configuration templates for six terminal coding agents (MIT): the V1 template's
disabled_providersline (opencode/v1/opencode.jsonc, line 22) and the V2 template's provider policy (opencode/v2/opencode.jsonc, line 842). - @opencode/cli on the npm registry: version list and publish dates (2.2.0 on 27 October 2026, 2.2.5 on 30 October 2026, UTC).
- opencode-disable-zen on the npm registry: the package exists; not installed, not reviewed.