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:
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 credentials