Found during a source audit of 9bf8f4c. Verified by reading the code. A PR is incoming.
Two separate path-resolution problems in packages/opencode/src/core/account-paths.ts, both ending with two documents fighting over one file.
1. The state path is derived with a suffix test
export function deriveStatePath(configPath: string): string {
return configPath.endsWith(ACCOUNT_FILE_NAME)
? join(dirname(configPath), ACCOUNT_STATE_FILE_NAME)
: `${configPath}.state.json`
}
ACCOUNT_FILE_NAME is openai-auth.json, and endsWith matches any filename ending in it:
OPENCODE_OPENAI_AUTH_FILE |
derived state path |
/tmp/openai-auth.json |
/tmp/openai-auth-state.json |
/tmp/team-openai-auth.json |
/tmp/openai-auth-state.json |
Two different rosters in one directory silently share one state document. The second writer overwrites the first's stored per-account fields, which invalidates those accounts.
The module's own header comment describes this hazard class:
if the copies ever drifted, a writer would take a lock on one path while writing another — mutual exclusion silently lost on a file holding credentials
A basename comparison instead of endsWith keeps distinct config paths mapping to distinct state paths.
2. An explicit state path is accepted even if it equals the config path
export function getAccountStatePath(configPath = getAccountStoragePath()) {
const explicit = process.env.OPENCODE_OPENAI_AUTH_STATE_FILE?.trim()
if (explicit) return explicit
return deriveStatePath(configPath)
}
Nothing checks the override against the config path. Point OPENCODE_OPENAI_AUTH_FILE and OPENCODE_OPENAI_AUTH_STATE_FILE at the same file and a mutation writes the config document and then the state document to it — the second write replaces the roster with a state-only document.
Affected
packages/opencode/src/core/account-paths.ts at 9bf8f4c, lines 38-48.
Found during a source audit of
9bf8f4c. Verified by reading the code. A PR is incoming.Two separate path-resolution problems in
packages/opencode/src/core/account-paths.ts, both ending with two documents fighting over one file.1. The state path is derived with a suffix test
ACCOUNT_FILE_NAMEisopenai-auth.json, andendsWithmatches any filename ending in it:OPENCODE_OPENAI_AUTH_FILE/tmp/openai-auth.json/tmp/openai-auth-state.json/tmp/team-openai-auth.json/tmp/openai-auth-state.jsonTwo different rosters in one directory silently share one state document. The second writer overwrites the first's stored per-account fields, which invalidates those accounts.
The module's own header comment describes this hazard class:
A basename comparison instead of
endsWithkeeps distinct config paths mapping to distinct state paths.2. An explicit state path is accepted even if it equals the config path
Nothing checks the override against the config path. Point
OPENCODE_OPENAI_AUTH_FILEandOPENCODE_OPENAI_AUTH_STATE_FILEat the same file and a mutation writes the config document and then the state document to it — the second write replaces the roster with a state-only document.Affected
packages/opencode/src/core/account-paths.tsat9bf8f4c, lines 38-48.