Bug type
Behavior bug (incorrect output/state without crash)
Summary
On OpenClaw v2026.3.2, a custom model provider configured with an environment-variable-backed API key is being materialized into models.json with the resolved plaintext secret value.
This appears to defeat the expected SecretRef / env-substitution behavior, because the secret is not only resolved at runtime, but also written back to disk in agent state.
I also see openclaw secrets audit --check reporting the corresponding config field as plaintext, even though the source config contains ${ENV_VAR} rather than a literal key.
Version
- OpenClaw:
2026.3.2
- Install method: global npm install
- OS: Debian 12
- Runtime: systemd service
- State dir:
/opt/openclaw/state
Steps to reproduce
Reproduction steps
1. Configure a custom provider in openclaw.json using env substitution:
"apiKey": "${LLM_API_KEY}"
2. Export the environment variable (or provide it through systemd EnvironmentFile).
3. Start OpenClaw.
4. Observe that a generated file appears:
/opt/openclaw/state/agents/main/agent/models.json
5. Inspect that file and see that the resolved secret is written as plaintext.
6. Delete that file and restart OpenClaw.
7. Observe that the file is recreated and the plaintext key is written again.
8. Run:
openclaw secrets audit --check
9. Observe that audit reports models.providers.llmmodel.apiKey in openclaw.json as plaintext even though it is ${LLM_API_KEY}.
Expected behavior
models.providers.<custom>.apiKey configured via ${LLM_API_KEY} or SecretRef should remain environment-backed / secret-backed.
- The resolved API key should not be written to disk in any generated state file such as:
.../agents/main/agent/models.json
openclaw secrets audit --check should not flag ${LLM_API_KEY} in openclaw.json as plaintext.
Actual behavior
After starting OpenClaw, a generated state file appears at:
/opt/openclaw/state/agents/main/agent/models.json
That file contains the resolved apiKey as a literal plaintext value, for example:
"llmmodel": {
"baseUrl": "https://model.supplier/api/v3",
"apiKey": "REDACTED_ACTUAL_SECRET",
"api": "openai-completions",
"models": [
{
"id": "llm-model-name",
"name": "LLMSAMPLE",
...
}
]
}
If I delete that generated models.json and restart the service, OpenClaw recreates it and writes the plaintext secret again.
Also, openclaw secrets audit --check reports:
[PLAINTEXT_FOUND] /opt/openclaw/state/openclaw.json:models.providers.providername.apiKey models.providers.providername.apiKey is stored as plaintext.
However, in openclaw.json, the field is not plaintext. It is configured as:
"apiKey": "${LLM_API_KEY}"
Minimal config snippet
This is the relevant part of openclaw.json:
{
"models": {
"mode": "merge",
"providers": {
"llmmodel": {
"baseUrl": "https://model.supplier/api/v3",
"api": "openai-completions",
"apiKey": "${LLM_API_KEY}",
### OpenClaw version
2026.3.2
### Operating system
Debian12-bookworm
### Install method
npm global
### Logs, screenshots, and evidence
```shell
Impact and severity
This is a secret hygiene / security issue because a runtime-resolved secret is being persisted to disk in plaintext in generated state.
Additional information
Additional notes
• I did not manually write the plaintext key into models.json.
• The only intended source of truth was:
• /etc/openclaw/secrets.env
• openclaw.json with ${LLM_API_KEY}
• This looks like either:
• a state-generation bug that persists resolved secrets into models.json, or
• an audit false positive for env-substituted custom provider apiKey, or
• both.
Bug type
Behavior bug (incorrect output/state without crash)
Summary
On OpenClaw
v2026.3.2, a custom model provider configured with an environment-variable-backed API key is being materialized intomodels.jsonwith the resolved plaintext secret value.This appears to defeat the expected SecretRef / env-substitution behavior, because the secret is not only resolved at runtime, but also written back to disk in agent state.
I also see
openclaw secrets audit --checkreporting the corresponding config field as plaintext, even though the source config contains${ENV_VAR}rather than a literal key.Version
2026.3.2/opt/openclaw/stateSteps to reproduce
Reproduction steps
1. Configure a custom provider in openclaw.json using env substitution:
"apiKey": "${LLM_API_KEY}"
2. Export the environment variable (or provide it through systemd EnvironmentFile).
3. Start OpenClaw.
4. Observe that a generated file appears:
/opt/openclaw/state/agents/main/agent/models.json
5. Inspect that file and see that the resolved secret is written as plaintext.
6. Delete that file and restart OpenClaw.
7. Observe that the file is recreated and the plaintext key is written again.
8. Run:
openclaw secrets audit --check
9. Observe that audit reports models.providers.llmmodel.apiKey in openclaw.json as plaintext even though it is ${LLM_API_KEY}.
Expected behavior
models.providers.<custom>.apiKeyconfigured via${LLM_API_KEY}or SecretRef should remain environment-backed / secret-backed..../agents/main/agent/models.jsonopenclaw secrets audit --checkshould not flag${LLM_API_KEY}inopenclaw.jsonas plaintext.Actual behavior
After starting OpenClaw, a generated state file appears at:
Impact and severity
This is a secret hygiene / security issue because a runtime-resolved secret is being persisted to disk in plaintext in generated state.
Additional information
Additional notes
• I did not manually write the plaintext key into models.json.
• The only intended source of truth was:
• /etc/openclaw/secrets.env
• openclaw.json with ${LLM_API_KEY}
• This looks like either:
• a state-generation bug that persists resolved secrets into models.json, or
• an audit false positive for env-substituted custom provider apiKey, or
• both.