anomalyco / anomalyco/opencode

[Bug] auth.json rewritten wholesale on Auth mutation — schema-incompatible credentials silently deleted; background metadata refresh triggers data loss

Open
#42,387 0 comments 0 reactions 1 assignee View on GitHub

@jlongster is already working on this.

Since Aug 13, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
Avg merge
7h 2m
Merged PRs (30d)
384

Description

Description

Summary

Every Auth.set() / Auth.remove() rewrites the ENTIRE ~/.local/share/opencode/auth.json.
Auth.all() silently discards every entry that fails schema decoding, so any single
mutation permanently deletes credentials whose JSON shape the running build doesn't
recognize. The Desktop app additionally performs scheduled background writes to the
same file (provider-metadata refresh — e.g. DigitalOcean routers, refreshed ~every 5
min, which calls auth.set and bumps metadata.routers_fetched_at), so schema-incompatible
credentials disappear later "by themselves" — no user action involved.

Environment

  • Windows 10
  • OpenCode CLI 1.18.18 (npm) AND Desktop app (@opencode-aidesktop) sharing one auth.json
  • First mass-loss observed right after upgrading into the v1.18.16 era

Verified mechanism (v1.18.15 source, confirmed present in v1.18.18)

packages/opencode/src/auth/index.ts:

Auth.all(): Record.filterMap(data, v => Result.fromOption(decode(v), () => undefined))
            // ^ entries failing schema decode are silently dropped
Auth.set(key, info):  writeJson(file, { ...all(), [norm]: info })   // FULL rewrite
Auth.remove(key):     writeJson(file, all())                         // FULL rewrite

No inter-process locking around the read→modify→write sequence.

Live reproduction (verified on 1.18.18, under 2 minutes, no real tokens)

  1. auth.json contains only digitalocean: { "type": "api", ... } (schema-valid).
  2. Inject one entry that fails the legacy OAuth schema (missing required fields):
    "decoy-bad-oauth": { "type": "oauth", "access": "fake" }
  3. Run opencode auth logout digitalocean (target: digitalocean ONLY)
  4. auth.json is now EMPTY — the decoy was deleted too, although remove never
    addressed it. No warning, no log entry. (Real credential restored from backup.)

This reproduces the production wipe signature exactly: one mutation erases every
entry the executing build cannot decode.

Writer analysis (production trigger)

  • opencode auth list leaves auth.json untouched (identical sha256 + mtime before/after).
  • With only the Desktop app running and zero user auth actions, auth.json was
    rewritten multiple times in one day; digitalocean.metadata.routers_fetched_at
    advanced (1786420850546 -> 1786644641977) — consistent with the DigitalOcean
    routers refresh path calling auth.set on a schedule.
  • Each such rewrite is a wipe event for every entry the running build's schema
    rejects (e.g. entries written by a different CLI/Desktop build).

Observed mass wipe

After upgrading to v1.18.16-era builds, auth.json lost ALL OAuth credentials
(openai OAuth, github-copilot OAuth, nvidia, fireworks). Only the type: "api"
static-key digitalocean entry survived. auth.json.bak (made post-incident) is
byte-identical to the wiped file.

What is NOT the cause

The V2 migration 20260805200742_import_legacy_credentials.ts explicitly supports
OAuth (converts to Credential.OAuth); #41607 only swapped Bun.file → node:fs
reads. The importer preserves OAuth. The loss happens in the legacy Auth
filter+rewrite path / V1↔V2 sync.
Also: v1.18.15...v1.18.16 contains 12 commits, none auth-related — the wipe
starting "after the update" is shape-drift-triggered, not version-triggered.

Related: #40920 (V1↔V2 credential consolidation drops creds stored in only one
store), #41820 (Copilot OAuth present but provider unregistered — different bug,
kept as context), #41173, #41607.

Suggested fixes (any mitigates; first is minimal)

  1. Preserve undecodable entries verbatim on save: merge the mutation into the RAW
    map, not the filtered map. Never silently delete credentials.
  2. Serialize read→modify→write access to auth.json with an inter-process lock or a
    single-writer model. (Add atomic temp+rename only as crash-safety against torn
    writes — it does not fix cross-process lost updates.)
  3. Stop scheduled Auth.set() for volatile metadata (routers, fetched_at) — store it
    outside the credential file.

Workaround for affected users

Move providers to static API keys via env vars or provider.<id>.options.apiKey
in opencode.json — bypasses auth.json entirely.

Plugins

No response

OpenCode version

1.18.18

Steps to reproduce

No response

Screenshot and/or share link

No response

Operating System

Windows 10

Terminal

PowerShell

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.