Skip to content

Instantly share code, notes, and snippets.

@rxbynerd
Created March 26, 2026 09:37
Show Gist options
  • Select an option

  • Save rxbynerd/b42042ba9ee15db6ef35a2c3fb28d1d5 to your computer and use it in GitHub Desktop.

Select an option

Save rxbynerd/b42042ba9ee15db6ef35a2c3fb28d1d5 to your computer and use it in GitHub Desktop.

OpenCode Debugging Guide

Practical guide for diagnosing configuration, provider, and credential issues in OpenCode.


Table of Contents


Quick Diagnostic Commands

# 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 System

How Config Loads

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 File Format Pitfalls

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

Environment Variable Substitution

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


Credentials & Auth

Where Credentials Live

~/.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

Credential Schema (Strict)

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"
}

Silent Validation Failures

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

Verifying Credentials

# 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 list

Provider Discovery

How Providers Are Found

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

Provider Filtering

opencode.json can restrict which providers are available:

{
  // Only allow these providers (if set, all others are blocked)
  "enabled_providers": ["anthropic", "openai"],

  // Block these specific providers
  "disabled_providers": ["openrouter"]
}

A provider blocked by disabled_providers is skipped during the entire discovery pipeline — its env vars and auth.json entries are ignored.

Common Provider Issues

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

Model Selection

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


Logging

Log Levels

DEBUG, INFO (default), WARN, ERROR.

Log Output

# 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/

Useful Log Messages to Grep For

# 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

Environment Variable Reference

Configuration

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

Debugging

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

Feature Flags

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment