Environment variables
The process environment selects the data directory and can supply API
keys. It does not replace config.json or providers.json.
| Variable | Meaning |
|---|---|
CRYSTAL_HOME |
Data directory instead of ~/.crystal |
<PROVIDER>_API_KEY |
Default key variable for a provider name |
apiKeyEnvironment |
Optional providers.json field that replaces the default variable name |
CRYSTAL_API_KEY |
Shared fallback when the provider requires a key and no earlier variable is set |
XDG_CONFIG_HOME |
Config root for the OpenCode instruction and skill fallbacks. Unset uses ~/.config |
PATH |
Inherited by bash. The host does not invent a path |
Do not pass secrets on the command line. They appear in process lists.
Data directory
Section titled “Data directory”CrystalHome.Resolve picks the root in this order:
- An explicit root argument, when the caller passes one.
CRYSTAL_HOME, when it is set and not whitespace. The value is passed throughPath.GetFullPathand is not trimmed.<user profile>/.crystal.
If the user profile directory is unavailable and neither of the first
two applies, startup throws The user profile directory is not available.
EnsureCreated creates the root plus sessions, logs, plugins, and
tools. Other well-known files (config.json, providers.json,
credentials.json, and the rest) are created when the feature that
owns them first writes.
API keys
Section titled “API keys”For one provider, CredentialStore stops at the first non-whitespace value:
- The variable named by
apiKeyEnvironment, when that field is set. Otherwise<NAME>_API_KEY: the provider name uppercased, hyphens turned into underscores.deepseekisDEEPSEEK_API_KEY.my-providerisMY_PROVIDER_API_KEY. - The derived
<NAME>_API_KEYagain, whenapiKeyEnvironmentnamed something else. A custom name does not hide the derived name. CRYSTAL_API_KEY, only when the provider requires an API key.apiKeyinproviders.json, when set.credentials.jsonunder the data directory, key<provider>.apiKey.
Environment values are trimmed. An empty or whitespace variable is skipped.
requiresApiKey defaults to false for ollama and true for every other protocol. If no key is required and none was found, resolution succeeds with an empty key. If a key is required and every source missed, startup reports the provider name, the apiKey forms, the environment names, and the credentials file path. /model leaves the current model unchanged when the selected provider has no key.
apiKey forms
Section titled “apiKey forms”apiKey is not read until the environment sources above are empty.
| Form | Behavior |
|---|---|
| Literal text | Used as the key after trim |
{env:NAME} |
Reads NAME. Missing or whitespace fails with Environment variable NAME is not set. |
{file:path} |
Reads the whole file and trims it. ~ and ~/... expand to the user profile. A relative path is under the data directory, not the workspace. Missing file: API key file not found: <path>. Empty file: API key file is empty: <path>. |
{env:NAME} is independent of <PROVIDER>_API_KEY. Use it when the secret already lives in a different variable.
Saving a key writes credentials.json and, on Unix, sets the file to owner read and write only. The save does not export the key into the environment.
XDG_CONFIG_HOME
Section titled “XDG_CONFIG_HOME”Instruction discovery and skill discovery share one rule. If XDG_CONFIG_HOME is set and not whitespace, that directory is the config root. Otherwise the root is <user profile>/.config.
Instructions then look for <config>/opencode/AGENTS.md as a global fallback, after the files in the Crystal data directory. Skills look under <config>/opencode as one OpenCode-compatible scan root. Project files under the workspace do not use this variable. See Prompts and instructions and Skills.