Description
Running openclaw doctor --fix resolves all ${...} environment variable references in openclaw.json to their plaintext values, writing secrets (API keys, bot tokens, passwords) directly into the config file.
Steps to Reproduce
- Have
openclaw.json with env var references like:
"token": "${DISCORD_BOT_TOKEN}"
- Have the corresponding values in
~/.openclaw/.env
- Run
openclaw doctor --fix
- Observe that
openclaw.json now contains:
All ${...} references in env.vars, channel tokens, gateway password, hooks token, and any other field using env substitution are resolved to plaintext.
Expected Behavior
doctor --fix should preserve ${...} references when writing the config back. Env var substitution should only happen in memory at runtime, never persisted to disk.
Root Cause
In src/commands/doctor-config-flow.ts (line ~187), doctor loads the config via readConfigFileSnapshot() and uses snapshot.config (the fully resolved version) rather than snapshot.parsed (which preserves ${...} references). When writeConfigFile() serializes cfg back to JSON, it writes the resolved values.
The resolution happens in src/config/io.ts:386 via resolveConfigEnvVars(), and there's no mechanism to restore the original ${...} syntax on write-back.
Key files:
src/config/io.ts — resolves env vars during read (line 386), writes resolved config (line 478)
src/config/env-substitution.ts — performs substitution (lines 46-100)
src/commands/doctor-config-flow.ts — uses snapshot.config instead of snapshot.parsed (line 187)
Suggested Fix
Option A: Have doctor work with snapshot.parsed (unresolved) and only resolve for validation, not for write-back.
Option B: Track which config values contained ${...} references and restore them before writing.
Related
#3261 describes the same underlying problem through the gateway restart code path. This issue covers the doctor --fix path specifically, but the root cause is shared: writeConfigFile() has no concept of preserving env var references.
Environment
- Version: 2026.1.29
- Platform: macOS (Darwin)
Description
Running
openclaw doctor --fixresolves all${...}environment variable references inopenclaw.jsonto their plaintext values, writing secrets (API keys, bot tokens, passwords) directly into the config file.Steps to Reproduce
openclaw.jsonwith env var references like:~/.openclaw/.envopenclaw doctor --fixopenclaw.jsonnow contains:All
${...}references inenv.vars, channel tokens, gateway password, hooks token, and any other field using env substitution are resolved to plaintext.Expected Behavior
doctor --fixshould preserve${...}references when writing the config back. Env var substitution should only happen in memory at runtime, never persisted to disk.Root Cause
In
src/commands/doctor-config-flow.ts(line ~187), doctor loads the config viareadConfigFileSnapshot()and usessnapshot.config(the fully resolved version) rather thansnapshot.parsed(which preserves${...}references). WhenwriteConfigFile()serializescfgback to JSON, it writes the resolved values.The resolution happens in
src/config/io.ts:386viaresolveConfigEnvVars(), and there's no mechanism to restore the original${...}syntax on write-back.Key files:
src/config/io.ts— resolves env vars during read (line 386), writes resolved config (line 478)src/config/env-substitution.ts— performs substitution (lines 46-100)src/commands/doctor-config-flow.ts— usessnapshot.configinstead ofsnapshot.parsed(line 187)Suggested Fix
Option A: Have doctor work with
snapshot.parsed(unresolved) and only resolve for validation, not for write-back.Option B: Track which config values contained
${...}references and restore them before writing.Related
#3261 describes the same underlying problem through the
gateway restartcode path. This issue covers thedoctor --fixpath specifically, but the root cause is shared:writeConfigFile()has no concept of preserving env var references.Environment