English | Espanol
All settings are environment variables. Defaults are safe: with no configuration at all, the plugin resolves the key from the standard stores and enables every hook.
| Variable | Default | Meaning |
|---|---|---|
COMMAND_CODE_API_KEY |
unset | Highest priority. Used as-is (trimmed, surrounding quotes stripped). |
COMMANDCODE_KEY_FILE |
unset | Absolute path to a file containing only the key. Ignored when the file is missing or empty, so the cascade keeps going. |
COMMANDCODE_API_KEYS |
unset | Comma or whitespace separated pool. Two or more keys enable rotation. |
COMMANDCODE_KEYS_FILE |
unset | Pool file, one label=key or bare key per line. # starts a comment. |
CMD_ZDR |
unset | Set to 1 to send x-cmd-zdr: 1 (zero data retention). |
XDG_CONFIG_HOME |
~/.config |
Base for the third resolution step: $XDG_CONFIG_HOME/commandcode/key.txt. |
COMMAND_CODE_API_KEY
-> COMMANDCODE_KEY_FILE
-> $XDG_CONFIG_HOME/commandcode/key.txt
-> /etc/environment (COMMAND_CODE_API_KEY)
-> ~/.commandcode/auth.json (apiKey | api_key | token | accessToken)
-> ~/.commandcode/credentials.json
-> ~/.commandcode/config.json
-> none (injection disabled, everything else keeps working)
resolveApiKeyWithSource() reports which store was used, which is what the tests
assert on. resolveApiKey() returns only the string.
| Variable | Default | Meaning |
|---|---|---|
COMMANDCODE_ALLOWED_HOSTS |
unset | Comma-separated hostnames that may also receive the key, in addition to api.commandcode.ai. |
Use this only for a self-hosted gateway that genuinely terminates your Command Code traffic. Every host added here can read the API key.
COMMANDCODE_ALLOWED_HOSTS=gateway.internal,proxy.lan
A pool of two or more keys activates rotation. With one key (or none) the plugin
keeps the single-key behaviour and never touches an Authorization header the
host already set.
| Source | Format |
|---|---|
COMMANDCODE_API_KEYS |
key1,key2 or whitespace separated |
COMMANDCODE_KEYS_FILE |
one label=key or bare key per line, # comments |
~/.config/commandcode/keys.txt |
same format, used when neither variable is set |
Cooldown recorded per key after a response that blames the credential:
| Status | Cooldown |
|---|---|
402 insufficient credits |
24h |
401 / 403 invalid or not entitled |
24h |
429 rate limited |
Retry-After when present and at most 6h, else 5m |
5xx |
none (not the key's fault) |
When every key is cooling down the one that recovers first is used, so the
request still reaches upstream. State lives in
<state>/commandcode-keys-state.json (mode 0600, labels only, never keys) and
each cooldown is appended to <state>/commandcode-rotation.log.
| commandcode-errors.log | append | JSON lines, one per decorated failure |
| Variable | Default | Meaning |
|---|---|---|
CC_NO_CONNECTION_CLOSE |
unset | Set to 1 to stop injecting Connection: close. |
sentinel file <plugin>/.disable-connection-close |
absent | Same effect as the variable, togglable without a restart. |
Both switches exist so the header can be disabled for measurement without editing
code. Disabling it restores keep-alive reuse, which reintroduces the stale-socket
ECONNRESET case if that is what you are hitting.
The watchdog timeout (60s after the first chunk) is a constant in
src/index.ts (WATCHDOG_TIMEOUT_MS), not an environment variable.
| Variable | Default | Meaning |
|---|---|---|
COMMANDCODE_STATE_DIR |
$XDG_STATE_HOME/opencode or ~/.local/state/opencode |
Directory for the three logs and the model cache. |
Files written under the state directory:
| File | Append/overwrite | Notes |
|---|---|---|
commandcode-sanitizer.log |
append | JSON lines, one per repaired payload |
commandcode-watchdog.log |
append | JSON lines, one per aborted stream |
commandcode-retries.log |
append | JSON lines, Command Code retries only |
commandcode-rotation.log |
append | JSON lines, one per key cooldown (label, status, reason, reset time) |
commandcode-keys-state.json |
overwrite | Cooldown state per key label (0600, no secrets) |
commandcode-models-cache.json |
overwrite | { at, models[] }, 6h TTL |
Logs are append-only and never rotated by the plugin. At the observed event rate
(tens of lines per week) this is not a concern; if you want rotation, point
COMMANDCODE_STATE_DIR at a directory managed by logrotate.
| Constant | Value | Location |
|---|---|---|
| API request timeout | 10s | DEFAULT_TIMEOUT_MS in src/client/api.ts |
| Catalog cache TTL | 6h | CATALOG_TTL_MS in src/client/api.ts |
| SSE inactivity window | 60s | WATCHDOG_TIMEOUT_MS in src/index.ts |
| Default output limit per model | 16384 | applyModelCatalog in src/index.ts |
The plugin registers itself; only the provider block belongs in opencode.json.
Install it with the GitHub specifier (no npm, no build), a local clone, or by dropping it
into the auto-discovered plugins directory - see the readme for the three options.
Notes:
- The
modelsmap inside the provider is optional. Anything listed there wins for the same id; discovery adds the rest at startup. - Keep
apiKeyin the provider block as a fallback for tooling that bypasses the plugin. The plugin resolves the key independently. - Do not load two plugins that register
commandcode_quotaat the same time.
| Variable | Default | Meaning |
|---|---|---|
COMMANDCODE_LIVE_TESTS |
unset | Set to 1 to run tests/live.test.ts, which calls the real API. |
bun test # offline, no quota consumed
COMMANDCODE_LIVE_TESTS=1 bun test # adds the live suite
{ "plugins": [ "github:JoshRob297/opencode-commandcode" ], "providers": { "commandcode": { "name": "Command Code", "package": "@opencode/ai/providers/openai-compatible", "settings": { "baseURL": "https://api.commandcode.ai/provider/v1", "apiKey": "{file:~/.config/commandcode/key.txt}" } } } }