OpenClaw environment variables
Why the service cannot see your API key
You export ANTHROPIC_API_KEY in your shell, run openclaw, and everything works. Then you install the Gateway as a systemd or launchd service, restart it, and the same key is gone. Nothing was deleted and nothing is broken: a service started by the operating system never sees the shell you typed that export into. OpenClaw loads environment variables from five ranked sources, and your terminal is only one of them.
The five sources, highest precedence to lowest
- Level one is the process environment, whatever the Gateway process already inherited from its parent shell or daemon. Everything under it only fills gaps, which is why an empty service environment stays empty until you feed it from somewhere else.
- Level two is a .env in the current working directory, the dotenv default. It does not override existing values, and OpenClaw strips provider credentials and protected runtime controls out of it before precedence is applied at all.
- Level three is the global .env at ~/.openclaw/.env, also reachable as $OPENCLAW_STATE_DIR/.env, and the docs recommend it for provider API keys. It does not override either, with one exception: for a systemd service that OpenClaw installed, it may replace service values OpenClaw itself recorded as managed, while operator-owned service values still take precedence.
- Level four is the env block in ~/.openclaw/openclaw.json, applied only if a key is missing, and level five is the optional login-shell import, which pulls in only missing expected keys. If the config file does not exist at all, level four is skipped and the shell import still runs when it is enabled.
Workspace .env files are a lower-trust source: OpenClaw ignores provider credentials and protected runtime controls from workspace .env before applying precedence.
Why a key in your project .env does nothing
This behaviour is deliberate and the blocked set is wide. It covers every known provider auth variable, including GEMINI_API_KEY, GOOGLE_API_KEY, XAI_API_KEY, MISTRAL_API_KEY, GROQ_API_KEY, DEEPSEEK_API_KEY, PERPLEXITY_API_KEY, BRAVE_API_KEY, TAVILY_API_KEY, EXA_API_KEY and FIRECRAWL_API_KEY. It also covers any key ending in _API_HOST, _BASE_URL, _ENDPOINT or _HOMESERVER, plus the entire OPENCLAW_*, CLAWHUB_*, ANTHROPIC_API_KEY_* and OPENAI_API_KEY_* namespaces. Ordinary project variables still load from a workspace .env; credentials, endpoint redirects, host overrides and runtime controls do not. Nothing fails loudly when you put a key there, which is exactly what makes the symptom so hard to recognise. If you already keep provider keys or endpoint routing values only in a workspace .env, move them: the trusted sources are the Gateway process environment, the global .env, the config env block, and the optional shell import.
Three ways to get the key to the service
- Write it into the global .env at ~/.openclaw/.env. That file is the documented home for provider API keys, it counts as a trusted source, and it loads whether or not the service inherited anything from your shell. It is also the file to use for the path variables, because OpenClaw drops the whole OPENCLAW_* namespace from an untrusted workspace .env.
- Put it in the config env block under env.vars, where values apply only if they are missing. That block takes literal strings only: a value like file:secrets/xai-api-key.txt is handed to the provider as exactly that text, and no file is read.
- Enable the login-shell import with env.shellEnv.enabled, or with OPENCLAW_LOAD_SHELL_ENV=1. OpenClaw then runs your login shell and imports only missing expected keys, with a 15000 ms timeout by default, and for Bash it uses an interactive login shell, so keep those startup files quiet and bounded.
One diagnostic trips people up here. The command openclaw models status reports whether shell import is enabled, so a line reading Shell env: off tells you OpenClaw will not read your login shell, not that your keys are missing. For file-backed keys, skip the config env block and put a SecretRef on the credential field instead, which is the subject of secrets management in OpenClaw. The env block is the blunt instrument, and the openclaw.json walkthrough covers the structure it sits inside.
What a service start keeps, and what it strips
On fresh Ubuntu installs that use the default state dir, OpenClaw also reads ~/.config/openclaw/gateway.env, a compatibility fallback consulted after the global .env; if the two disagree, OpenClaw keeps ~/.openclaw/.env and prints a warning. Systemd startup preserves managed process values that config still references, including dollar-sign SecretRef shorthand inside $include files, and values supplied by an operator EnvironmentFile line. Managed values that appear in neither a trusted dotenv file nor a current config reference are removed from the Gateway process environment, so deleting a key from the global .env really does take it out of the running service. One more thing worth knowing: OpenClaw reads only OPENCLAW_* variables, and the legacy CLAWDBOT_* and MOLTBOT_* prefixes are silently ignored, with a single deprecation warning if any are still set. If the Gateway process itself is new to you, the Gateway explained explains what it is, and model authentication covers where provider credentials are meant to live.
On Diali
Diali hosts OpenClaw, so this whole ladder is settled before your assistant boots. Each customer runs their own assistant on its own runtime, the runtime configuration is generated from the dashboard and replaced at every release, and provider keys are entered there rather than in a file you have to remember to edit on a server. Working state lives on a persistent volume, with daily snapshots and one-click restore available through the Backups add-on (included on Max). Hosted OpenClaw on Diali sets out what the hosted runtime includes, and Diali pricing is where the plans are.
- The process environment wins, and every level below it only fills gaps.
- Provider keys belong in ~/.openclaw/.env, never in a workspace .env.
- A service inherits nothing from your shell until you enable the import.
Stop reading about it, build one
Set up an agent, pick a channel, and have it working inside the app you already keep open.
