Practical guide for diagnosing configuration, provider, and credential issues in OpenCode.
- Quick Diagnostic Commands
- Configuration System
- Credentials & Auth
- Provider Discovery
- Model Selection
- Logging
- Environment Variable Reference
# Dump the fully resolved configuration
opencode debug config
# Show XDG directory paths (data, config, state, cache, log)
opencode debug paths
# List stored credentials and active env vars
opencode providers list
# Run with verbose logging to stderr
opencode --print-logs --log-level DEBUG
# Validate auth.json is parseable
cat ~/.local/share/opencode/auth.json | python3 -m json.tool
# Check which env vars are set for providers
env | grep -iE '(API_KEY|ANTHROPIC|OPENAI|GEMINI|OPENROUTER)'Configuration is merged from multiple sources in a strict precedence order (low to high):
| Priority | Source | Location |
|---|---|---|
| 1 | Remote well-known | https://<host>/.well-known/opencode |
| 2 | Global user config | ~/.config/opencode/opencode.json{,c} |
| 3 | Custom config flag | OPENCODE_CONFIG env var points to a file |
| 4 | Project config | opencode.json{,c} in project root |
| 5 | .opencode dirs |
.opencode/opencode.json{,c} (walked upward from cwd) |
| 6 | Inline config | OPENCODE_CONFIG_CONTENT env var (raw JSON string) |
| 7 | Remote account config | Enterprise org config |
| 8 (highest) | Managed config | Platform system dir (enterprise admin-controlled) |
Source: packages/opencode/src/config/config.ts:81-88
Higher-priority sources override lower ones via deep merge. Arrays like plugin and instructions are concatenated and deduplicated rather than replaced.
Config files (opencode.json, opencode.jsonc) are parsed with jsonc-parser, which supports:
- Comments (
//and/* */) - Trailing commas
However, auth.json is parsed with standard JSON.parse — no comments, no trailing commas. A malformed auth.json is silently replaced with {} (empty object) due to a .catch(() => ({})) at auth/index.ts:61.
Symptom: opencode providers list shows 0 credentials.
Fix: Validate your auth.json with python3 -m json.tool or jq ..
Config files support two substitution syntaxes:
| Syntax | Behavior | On Missing |
|---|---|---|
{env:VAR_NAME} |
Replaced with env var value | Silently becomes empty string |
{file:path/to/file} |
Replaced with file contents | Throws ConfigInvalidError |
The silent empty-string behavior for {env:} can mask misconfiguration. If a provider API key is injected this way and the var is unset, the key will be an empty string with no error.
Source: packages/opencode/src/config/paths.ts:84-141
~/.local/share/opencode/auth.json
This path is derived from XDG_DATA_HOME (defaults to ~/.local/share). If XDG_DATA_HOME or OPENCODE_TEST_HOME is set, the actual path will differ.
Run opencode debug paths to confirm the real path, or check the header printed by opencode providers list.
File permissions are set to 0o600 (owner read/write only) on every write.
Source: packages/opencode/src/auth/index.ts:10,78
Every entry in auth.json must match exactly one of these three schemas. Extra fields are tolerated, but missing or wrongly-typed required fields cause the entry to be silently dropped.
API Key:
{
"type": "api",
"key": "sk-..."
}OAuth:
{
"type": "oauth",
"refresh": "refresh-token-string",
"access": "access-token-string",
"expires": 1735689600
}Optional fields: accountId (string), enterpriseUrl (string).
Well-Known (enterprise/self-hosted):
{
"type": "wellknown",
"key": "ENV_VAR_NAME",
"token": "the-actual-token"
}This is the single most common cause of "providers list shows nothing" issues.
Auth.all() reads auth.json, then runs each entry through an Effect Schema.decodeUnknownOption discriminated union. Entries that fail validation are silently dropped — no error, no warning, no log line.
Source: packages/opencode/src/auth/index.ts:56-62
const decode = Schema.decodeUnknownOption(Info)
// ...
const data = await Filesystem.readJson<Record<string, unknown>>(file).catch(() => ({}))
return Record.filterMap(data, (value) => Result.fromOption(decode(value), () => undefined))Record.filterMap discards any entry where decode returns Option.none().
Common mistakes that trigger silent drops:
| Mistake | Example | Why It Fails |
|---|---|---|
Wrong type value |
"type": "apiKey" |
Must be exactly "api", "oauth", or "wellknown" |
Missing type field |
{ "key": "sk-..." } |
Discriminator field is required |
| Wrong field name | "apiKey": "sk-..." instead of "key" |
Schema expects key for api type |
| Wrong value type | "expires": "2025-01-01" |
Must be a number (epoch seconds) |
| Missing required field | { "type": "oauth", "access": "..." } |
oauth requires refresh, access, AND expires |
| Nested structure | { "anthropic": { "key": "..." } } |
Top-level keys are provider IDs; values must be flat credential objects |
# 1. Check file exists and is valid JSON
cat ~/.local/share/opencode/auth.json | python3 -m json.tool
# 2. Check each entry has a valid "type" discriminator
cat ~/.local/share/opencode/auth.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
for provider, cred in data.items():
t = cred.get('type', '<MISSING>')
valid = t in ('api', 'oauth', 'wellknown')
status = 'OK' if valid else 'INVALID'
print(f' {status}: {provider} (type={t})')
if t == 'api' and 'key' not in cred:
print(f' -> missing required field: key')
if t == 'oauth':
for f in ('refresh', 'access', 'expires'):
if f not in cred:
print(f' -> missing required field: {f}')
if 'expires' in cred and not isinstance(cred['expires'], (int, float)):
print(f' -> expires must be a number, got {type(cred[\"expires\"]).__name__}')
if t == 'wellknown':
for f in ('key', 'token'):
if f not in cred:
print(f' -> missing required field: {f}')
"
# 3. Compare what OpenCode actually sees
opencode providers listProvider resolution merges five sources (in provider/provider.ts:1045-1114):
models.dev database # Remote catalog of known providers + models
|
v
Environment variables # Scans each provider's env var list (e.g. ANTHROPIC_API_KEY)
|
v
auth.json credentials # Stored OAuth/API keys
|
v
Plugin loaders # Plugin-registered custom providers
|
v
Custom loaders # Built-in special handling (Anthropic, Azure, Bedrock, etc.)
|
v
Config file overrides # opencode.json "provider" section
Each provider is tagged with a source: "env", "api", "config", "custom", or "plugin".
A provider only appears as available if it has credentials from at least one source.
opencode.json can restrict which providers are available:
A provider blocked by disabled_providers is skipped during the entire discovery pipeline — its env vars and auth.json entries are ignored.
| Symptom | Likely Cause | Fix |
|---|---|---|
| Provider not listed | No credential source found | Set env var or run opencode providers login |
| Provider listed but no models | models.dev fetch failed or provider not in catalog |
Check network; try OPENCODE_MODELS_PATH for local override |
| Provider key rejected at runtime | Key stored correctly but provider API rejects it | Key may be expired/revoked; re-run opencode providers login |
| Provider visible in env but not in list | disabled_providers in config |
Check opencode debug config for disabled_providers |
| Custom provider ignored | Missing config entry | Custom providers need both a credential and a provider entry in opencode.json |
The last-used model is remembered separately by the TUI and web app.
TUI: ~/.local/state/opencode/model.json
{
"recent": [{ "providerID": "anthropic", "modelID": "claude-sonnet-4-6" }],
"favorite": [],
"variant": {}
}Max 10 recent entries. Fallback order: agent override > config model > recent list > first available.
Web app: localStorage key model-selection (workspace-scoped), plus global model key for recency (max 5).
DEBUG, INFO (default), WARN, ERROR.
# Logs to stderr (useful for piping/debugging)
opencode --print-logs --log-level DEBUG
# Log files (auto-rotated, keeps last 10)
ls ~/.local/share/opencode/log/# Config loading trace
grep -i "config" ~/.local/share/opencode/log/*.log
# Specific messages:
# "fetching remote config" — well-known config fetch
# "loaded custom config" — OPENCODE_CONFIG file
# "loading config from .opencode/" — directory config
# "failed to fetch remote account" — enterprise config error
# "Provider does not exist" — provider ID not in models.dev| Variable | Purpose |
|---|---|
OPENCODE_CONFIG |
Path to a custom config file |
OPENCODE_CONFIG_DIR |
Override config directory |
OPENCODE_CONFIG_CONTENT |
Inline JSON config string |
OPENCODE_TUI_CONFIG |
TUI-specific config file path |
OPENCODE_PERMISSION |
JSON override for permission settings |
OPENCODE_DISABLE_PROJECT_CONFIG |
Skip project-level config loading |
| Variable | Purpose |
|---|---|
OPENCODE_STRICT_CONFIG_DEPS |
Fail on config dependency install errors (default: warn) |
OPENCODE_MODELS_PATH |
Use a local models file instead of fetching from models.dev |
OPENCODE_MODELS_URL |
Custom models.dev endpoint |
OPENCODE_DISABLE_MODELS_FETCH |
Don't fetch remote model catalog |
OPENCODE_DB |
Override database path |
OPENCODE_SKIP_MIGRATIONS |
Skip database migrations |
OPENCODE_FAKE_VCS |
Fake VCS for testing |
| Variable | Purpose |
|---|---|
OPENCODE_EXPERIMENTAL |
Enable all experimental features |
OPENCODE_ENABLE_EXPERIMENTAL_MODELS |
Show experimental models |
OPENCODE_EXPERIMENTAL_LSP_TOOL |
Enable LSP tool |
OPENCODE_EXPERIMENTAL_PLAN_MODE |
Enable plan mode |
OPENCODE_DISABLE_AUTOCOMPACT |
Disable session auto-compaction |
OPENCODE_DISABLE_PRUNE |
Disable pruning of old tool outputs |
OPENCODE_DISABLE_DEFAULT_PLUGINS |
Skip default plugin loading |
OPENCODE_DISABLE_LSP_DOWNLOAD |
Don't auto-download LSP servers |
OPENCODE_DISABLE_AUTOUPDATE |
Disable auto-updates |
{ // Only allow these providers (if set, all others are blocked) "enabled_providers": ["anthropic", "openai"], // Block these specific providers "disabled_providers": ["openrouter"] }