Skip to content

[Bug]: [Security] [2026.3.8]Heartbeat-triggered agent activation persists custom provider API key as plaintext in agent-local models.json #42355

@samtxxx

Description

@samtxxx

Bug type

Behavior bug (incorrect output/state without crash)

Summary

On OpenClaw 2026.3.8, a custom OpenAI-compatible provider backed by an environment variable can still be materialized into plaintext inside agent-local models.json.

In my deployment, the first system-triggered heartbeat activation of agent chempro created:

  • a main session for that agent with systemSent: true and provider: heartbeat
  • an agent-local models.json

That models.json persisted the custom provider API key as a resolved plaintext value.

By contrast, other agents that later generated models.json through different activation paths only stored the environment variable name (LLM_API_KEY) rather than the resolved secret value.

So the issue appears to be path-dependent:

  • heartbeat/system auto-activation path → writes resolved plaintext API key
  • other activation paths → writes env var marker only

This looks like an incomplete fix for the previously reported SecretRef/custom-provider persistence bug.


Version

  • OpenClaw: 2026.3.8 (3caab92)

Environment

  • Debian 12 in PVE LXC
  • Gateway running as systemd service
  • Multi-agent setup
  • Custom OpenAI-compatible provider using Volcengine Ark endpoint:
    • https://ark.cn-beijing.volces.com/api/v3
  • API key configured via environment variable / SecretRef path
  • models.mode: "merge"

Steps to reproduce

  1. Configure the custom provider in openclaw.json with env/SecretRef-backed apiKey.
  2. Start the gateway.
  3. Do not send any Telegram message to chempro.
  4. Wait for the system heartbeat / auto-activation path.
  5. Check:
    cat /opt/openclaw/state/agents/chempro/sessions/sessions.json
    sed -n '1,120p' /opt/openclaw/state/agents/chempro/agent/models.json
  6. Observe that:
    • sessions.json shows a system-created heartbeat session:
    • systemSent: true
    • origin.provider: "heartbeat"
    • deliveryContext.to: "heartbeat"
    • chempro/agent/models.json is created
    • apiKey is persisted as a resolved plaintext secret
    Comparison
    Other agents later generated models.json too, but their files stored only the env var name:
    "apiKey": "LLM_API_KEY"
    while chempro/agent/models.json stored:
    "apiKey": "<REDACTED_PLAINTEXT_SECRET>"

Expected behavior

For custom providers whose API key originates from env / SecretRef, agent-local models.json should never persist the resolved secret value as plaintext.

At most, the file should contain:

  • a source marker
  • an env var identifier (for example LLM_API_KEY)
  • or some non-secret placeholder

It should not write the actual API key to disk.

Actual behavior

A system-generated heartbeat activation for chempro created:

  • sessions.json entry showing a heartbeat-originated system session
  • agent/models.json containing a resolved plaintext API key

At the same time, other agents generated models.json files that referenced the same provider but did not persist the resolved key; they stored LLM_API_KEY instead.

This inconsistency suggests the secret handling differs depending on activation path.

OpenClaw version

2026.3.8

Operating system

Debian12

Install method

npm global

Model

OpenAI-compatible

Provider / routing chain

openclaw -> volcano engine -> doubao-seed-2-0-pro-260215

Config file / key location

No response

Additional provider/model setup details

No response

Logs, screenshots, and evidence

Impact and severity

No response

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingbug:behaviorIncorrect behavior without a crashsecuritySecurity documentation

    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