You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
bug(v2): stale stored credential silently overrides provider options.apiKey; auth.json edits ignored after one-time import; no non-interactive key rotation #58394
On v2 the provider API key is resolved from the credential table in ~/.local/share/opencode/opencode.db, and that row silently wins over provider.<id>.options.apiKey from config. After rotating a key in opencode.json, requests keep being sent with the old stored key and fail with Authentication Fails, Your api key: ****<last4> is invalid. There is no warning that the two disagree, and no non-interactive way to fix the stored credential.
Three distinct problems, all hit while rotating a key:
Precedence is silent. A stored credential row beats provider.<id>.options.apiKey even when the config key is the only valid one. Setting the stored credential to an invalid value while a valid key sits in config makes the request fail with the stored key's last 4 chars, i.e. the config value is never used.
auth.json is dead after the one-time migration.20260805200742_import_legacy_credentials imports ~/.local/share/opencode/auth.json once and then skips every integration that already has a row (SELECT id FROM credential WHERE integration_id = ? → continue). Editing auth.json afterwards has no effect, but the file still looks like the source of truth.
No non-interactive rotation path.
opencode auth login <provider> --answer key=... → Unknown form field: key (also tried apiKey, apikey, api_key, API key)
opencode auth logout <credential-id> → Pass a credential ID or label when running without an interactive terminal, even when the explicit credential ID is passed
Rotating a key from a script / CI / container therefore requires a manual sqlite3 UPDATE.
Related but different: #54709 (preserve provider credential availability), #56126 (SAP AI Core — apiKey/serviceKey mismatch with valid saved credentials).
Steps to reproduce
opencode auth login <provider> with any key → creates a credential row.
Put a different, valid key in config as provider.<id>.options.apiKey.
opencode run hi → the request still uses the stored credential. With a stale stored key this fails with ****<last4 of stored key> is invalid.
Now set the stored credential to an invalid value and keep the valid key in config:
sqlite3 ~/.local/share/opencode/opencode.db \
"update credential set value=json_set(value,'$.key','sk-INVALID') where integration_id='<provider>'"
opencode run hi
→ still fails with ****ALID is invalid, proving options.apiKey is ignored.
Edit ~/.local/share/opencode/auth.json to the new key and repeat step 3 → no change; the file is only read by the legacy migration.
Reproduces with "plugin": [], so it is core, not plugin behaviour.
One more detail: the fix only took effect after killing the background service (opencode serve --service). The running service kept using the old credential in memory. If that is not intended, it is a fourth (smaller) problem: credential changes are not picked up until the service restarts.
Screenshot
n/a — CLI only, error text quoted above.
Plugins
oh-my-opencode-slim 2.2.24, but the issue reproduces with no plugins enabled.
In short, you'd like rotating a provider key to be straightforward and visible: a warning (or configurable precedence) when a stored credential overrides provider.<id>.options.apiKey, and a non-interactive way to replace a stored key, e.g. auth login --api-key, an overwrite flag on auth import, or an in-place update. Maintainers will consider it.
Some context from the current code:
A stored credential winning over options.apiKey is intended. The model resolver drops the config key whenever a credential exists, and the docs describe options.apiKey as for use when you're not using auth. The silent part, with no warning that the two disagree, is a fair point.
auth.json is only read by the one-time legacy import. After that the credential table is the source of truth.
opencode auth logout takes [target] [credential], and target is the integration. A credential ID passed alone ends up in the target slot. This should work without a terminal:
Independently hit this on v2.2.6 (Windows) and can confirm all three problems, plus one extra finding.
Symptom that made it hard to diagnose: my opencode.jsonc had
"minimax-cn": {
"options": {
"apiKey": "{env:MINIMAX_API_KEY}", // fresh, valid key — verified 200 via raw curl"baseURL": "https://api.minimaxi.com/anthropic/v1"
}
}
but the TUI kept failing with the provider's misleading error insufficient balance (1008). Packet-captured the outgoing request: opencode was sending a completely different, stale sk-cp-... key — the stored credential — and the remote then reported a billing error instead of an auth error, which sent me down a completely wrong path (tested streaming, cache_control, tools, max_tokens... everything, because the same key worked fine via curl).
Extra finding beyond auth.json: on v2.2.6 there is a second credential mirror, ~/.local/share/opencode/account.json ("version": 2, with accounts / active maps). Editing/deleting entries there does not take effect either while the real source of truth is the credential table in ~/.local/share/opencode/opencode.db. Same silent-override behavior: stored row beats provider.<id>.options.apiKey, with zero warning that the two disagree.
Non-interactive workaround that worked for me (since no CLI rotation path worked): back up the db, then
DELETEFROM credential WHERE integration_id ='minimax-cn';
then restart the opencode process. After that the {env:...} config key is picked up again and requests succeed. (Full restart of the running instance is required — the server keeps the old key in memory.)
What would fix this UX-wise, roughly in order of value:
Warn (or fail loudly) at startup when a stored credential and options.apiKey disagree, naming which one wins.
A non-interactive rotation command, e.g. opencode auth login <provider> --api-key <key> / opencode auth logout <provider> that targets the db table.
Log which credential source (db row vs env vs config) was used for each provider request — one line is enough; it would have cut my debugging from hours to minutes.
Activity
opencode-agent commentedon Oct 23, 2026
Thanks for the detailed write-up!
In short, you'd like rotating a provider key to be straightforward and visible: a warning (or configurable precedence) when a stored credential overrides
provider.<id>.options.apiKey, and a non-interactive way to replace a stored key, e.g.auth login --api-key, an overwrite flag onauth import, or an in-place update. Maintainers will consider it.Some context from the current code:
options.apiKeyis intended. The model resolver drops the config key whenever a credential exists, and the docs describeoptions.apiKeyas for use when you're not using auth. The silent part, with no warning that the two disagree, is a fair point.auth.jsonis only read by the one-time legacy import. After that thecredentialtable is the source of truth.opencode auth logouttakes[target] [credential], andtargetis the integration. A credential ID passed alone ends up in thetargetslot. This should work without a terminal:Related issues:
opencode auth logincredential.updatecurrently drops the new valueserve --servicekeeps using an old key after rotationauth login/logoutapiKey/serviceKeymismatch with saved credentialsskywalker-35 commentedon Nov 11, 2026
Independently hit this on v2.2.6 (Windows) and can confirm all three problems, plus one extra finding.
Symptom that made it hard to diagnose: my
opencode.jsonchadbut the TUI kept failing with the provider's misleading error
insufficient balance (1008). Packet-captured the outgoing request: opencode was sending a completely different, stalesk-cp-...key — the stored credential — and the remote then reported a billing error instead of an auth error, which sent me down a completely wrong path (tested streaming,cache_control,tools,max_tokens... everything, because the same key worked fine via curl).Extra finding beyond auth.json: on v2.2.6 there is a second credential mirror,
~/.local/share/opencode/account.json("version": 2, withaccounts/activemaps). Editing/deleting entries there does not take effect either while the real source of truth is thecredentialtable in~/.local/share/opencode/opencode.db. Same silent-override behavior: stored row beatsprovider.<id>.options.apiKey, with zero warning that the two disagree.Non-interactive workaround that worked for me (since no CLI rotation path worked): back up the db, then
then restart the opencode process. After that the
{env:...}config key is picked up again and requests succeed. (Full restart of the running instance is required — the server keeps the old key in memory.)What would fix this UX-wise, roughly in order of value:
options.apiKeydisagree, naming which one wins.opencode auth login <provider> --api-key <key>/opencode auth logout <provider>that targets the db table.