Skip to content

[Bug]: v2026.3.2: custom provider apiKey from env/SecretRef is persisted as plaintext into state agents/*/agent/models.json #38757

@samtxxx

Description

@samtxxx

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

  1. models.providers.<custom>.apiKey configured via ${LLM_API_KEY} or SecretRef should remain environment-backed / secret-backed.
  2. The resolved API key should not be written to disk in any generated state file such as:
    • .../agents/main/agent/models.json
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingbug:behaviorIncorrect behavior without a crash

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions