Skip to content

bug(v2): stale stored credential silently overrides provider options.apiKey; auth.json edits ignored after one-time import; no non-interactive key rotation #58394

Open

Description

@HeZ2z

Description

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:

  1. 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.

  2. 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.

  3. 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
    • opencode auth import exported.json → Imported 0 credentials, skipped 2 already present (no force/overwrite flag)

    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

  1. opencode auth login <provider> with any key → creates a credential row.
  2. Put a different, valid key in config as provider.<id>.options.apiKey.
  3. opencode run hi → the request still uses the stored credential. With a stale stored key this fails with ****<last4 of stored key> is invalid.
  4. 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.
  5. 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.

OpenCode version

v2.0.30 (CLI), background service same version

Operating System

macOS 15.8.1 (arm64)

Terminal

iTerm2

Activity

  1. opencode-agent commented on Oct 23, 2026

    @opencode-agent
    Contributor

    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 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:
      opencode auth logout <provider> <credential-id>
      You can then add the new key back. Non-interactive key entry for that step is what [FEATURE]: Support non-interactive API-key login via auth login #57492 covers.

    Related issues:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions