-
-
Notifications
You must be signed in to change notification settings - Fork 69.6k
[Bug]: v2026.3.2: custom provider apiKey from env/SecretRef is persisted as plaintext into state agents/*/agent/models.json #38757
Description
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>.apiKeyconfigured 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 --checkshould not flag${LLM_API_KEY}inopenclaw.jsonas 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.