Skip to content

[Bug] Race condition in auth.json causes provider credentials to disappear #37

Description

@randomm

Problem

Users report that providers keep disappearing from ~/.local/share/opencode/auth.json. Investigation reveals race conditions and error handling flaws in the authentication storage system.

Root Cause Analysis

1. Non-Atomic Read-Modify-Write Pattern

Auth.set() in packages/opencode/src/auth/index.ts:

exportasyncfunctionset(key: string,info: Info){constfile=Bun.file(filepath)constdata=awaitall()// READawaitBun.write(file,JSON.stringify({ ...data,[key]: info},null,2))// WRITEawaitfs.chmod(file.name!,0o600)}

Problem: Between read and write, another process can write, causing data loss.

2. Silent Failure Returns Empty Object

Auth.all() silently returns {} on any read failure:

constdata=awaitfile.json().catch(()=>({})asRecord<string,unknown>)

Problem: If file is corrupted/unreadable, next Auth.set() writes empty object + new provider, erasing all existing providers.

3. No File Locking

A Lock utility exists at util/lock.ts but is not used by the auth module. Multiple processes compete for the same file:

  • CLI TUI
  • Dev server (bun dev -- serve)
  • Web app (via SDK)
  • Desktop app
  • Multiple terminal windows

4. Race Condition Timeline

TimeProcess AProcess BFile State
T1Reads: { p1, p2 }-{ p1, p2 }
T2Adding p3...Reads: { p1, p2 }{ p1, p2 }
T3Writes: { p1, p2, p3 }-{ p1, p2, p3 }
T4DoneWrites: { p1, p2 }{ p1, p2 }p3 lost

Key Files

FilePurpose
packages/opencode/src/auth/index.tsCore auth.json read/write operations
packages/opencode/src/provider/auth.tsProvider auth flow (OAuth/API key)
packages/opencode/src/util/lock.tsLock utility (NOT used by auth)

Suggested Fix

Option 1: Use Existing Lock Utility

import{Lock}from"../util/lock"exportasyncfunctionset(key: string,info: Info){constrelease=awaitLock.write("auth")try{constfile=Bun.file(filepath)constdata=awaitall()awaitBun.write(file,JSON.stringify({ ...data,[key]: info},null,2))awaitfs.chmod(file.name!,0o600)}finally{release()}}

Option 2: Atomic Write with Rename

exportasyncfunctionset(key: string,info: Info){constfile=Bun.file(filepath)consttempFile=filepath+".tmp."+crypto.randomUUID()constdata=awaitall()awaitBun.write(tempFile,JSON.stringify({ ...data,[key]: info},null,2))awaitfs.chmod(tempFile,0o600)awaitfs.rename(tempFile,filepath)// Atomic on most filesystems}

Option 3: Better Error Handling

exportasyncfunctionall(): Promise<Record<string,Info>>{constfile=Bun.file(filepath)if(!awaitfile.exists())return{}try{constdata=awaitfile.json()// ... validation}catch(e){log.error("Failed to read auth.json, preserving existing file",{error: e})throwe// Don't silently return empty object!}}

Priority

High - Data loss affecting user credentials

Reproduction

  1. Open two terminal windows
  2. Run bun dev in both (or bun dev + bun dev -- serve)
  3. Add a provider in one terminal
  4. Add a different provider in the other terminal quickly
  5. Check auth.json - one provider may be missing

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions