openclaw - 💡(How to fix) Fix [Bug][Security][Regression] models status --probe rewrites agent models.json with resolved plaintext apiKey for non-CORE custom providers (2026.5.18)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

On OpenClaw 2026.5.18, running openclaw models status --probe (or any flow that triggers models.json regeneration) writes the resolved plaintext value of providers.<custom>.apiKey into agents/<id>/agent/models.json whenever the provider is not in the CORE allowlist (anthropic, openai, voyage, cerebras, anthropic-openai, qwen-dashscope). This appears to be a regression of #38757 / #42355 / #67719 — all closed.

A separate open ticket #84376 covers the codex codex-app-server sentinel sub-case (1 of 5 in our setup); this report covers the broader custom-provider class.

Root Cause

On OpenClaw 2026.5.18, running openclaw models status --probe (or any flow that triggers models.json regeneration) writes the resolved plaintext value of providers.<custom>.apiKey into agents/<id>/agent/models.json whenever the provider is not in the CORE allowlist (anthropic, openai, voyage, cerebras, anthropic-openai, qwen-dashscope). This appears to be a regression of #38757 / #42355 / #67719 — all closed.

A separate open ticket #84376 covers the codex codex-app-server sentinel sub-case (1 of 5 in our setup); this report covers the broader custom-provider class.

Fix Action

Fix / Workaround

Workaround (current)

For real external secrets, migrate the provider's apiKey out of models.json cache by using openclaw models auth paste-token --provider <id> --agent <id> — this persists in auth-profiles.json (which uses keyRef/SecretRef properly) and lets the regenerator write apiKey: "secretref-managed" (accepted marker). Required for every (agent × custom-provider-with-real-secret) pair — verbose but the only currently safe path.

Code Example

"xiaomi": {
     "apiKey": { "source": "env", "provider": "default", "id": "OPENCLAW_MODEL_XIAOMI_API_KEY" },
     ...
   }

---

"xiaomi": { "apiKey": "sk-eg3zcww2ytss...", ... }
   "litellm": { "apiKey": "sk-litellm", ... }
   "anthropic-proxy": { "apiKey": "proxy-passthrough", ... }
   "codex": { "apiKey": "codex-app-server", ... }

---

"anthropic": { "apiKey": "ANTHROPIC_API_KEY", ... }
RAW_BUFFERClick to expand / collapse

Summary

On OpenClaw 2026.5.18, running openclaw models status --probe (or any flow that triggers models.json regeneration) writes the resolved plaintext value of providers.<custom>.apiKey into agents/<id>/agent/models.json whenever the provider is not in the CORE allowlist (anthropic, openai, voyage, cerebras, anthropic-openai, qwen-dashscope). This appears to be a regression of #38757 / #42355 / #67719 — all closed.

A separate open ticket #84376 covers the codex codex-app-server sentinel sub-case (1 of 5 in our setup); this report covers the broader custom-provider class.

Environment

  • openclaw 2026.5.18 (commit 50a2481)
  • Debian 12 ARM64 (Hetzner CAX11), node v22.22.0
  • Gateway via systemd --user service
  • 5 agents (main, dev, dev-debug, seo, trading), 5 custom providers configured: litellm, xiaomi, anthropic-proxy, codex (legacy alias of openai-codex), plus CORE anthropic
  • openclaw.json.secrets.providers.default = {source: "env"} and openclaw.json.models.providers.*.apiKey already SecretRef objects pointing to OPENCLAW_MODEL_*_API_KEY env vars in ~/.openclaw/.env

Reproduction

  1. Start from per-agent models.json containing only legacy markers (e.g. "apiKey": "OPENCLAW_MODEL_XIAOMI_API_KEY" strings — initial state after Doc-era migration; 2026.5.x audit rightly flags as PLAINTEXT_FOUND).
  2. Replace with valid SecretRef objects:
    "xiaomi": {
      "apiKey": { "source": "env", "provider": "default", "id": "OPENCLAW_MODEL_XIAOMI_API_KEY" },
      ...
    }
    Audit reports plaintextCount: 0, unresolvedRefCount: 25 (REF_UNRESOLVED — "models.json contains an unresolved SecretRef object; regenerate models.json.").
  3. openclaw secrets reload succeeds. Runtime probe shows providers resolve correctly (effective.kind = "models.json", detail = "sk-eg3zcww...").
  4. Run openclaw models status --probe (no --agent, defaults to main).
  5. Actual: agents/main/agent/models.json is rewritten. For each custom provider, OpenClaw writes the resolved value in clear:
    "xiaomi": { "apiKey": "sk-eg3zcww2ytss...", ... }
    "litellm": { "apiKey": "sk-litellm", ... }
    "anthropic-proxy": { "apiKey": "proxy-passthrough", ... }
    "codex": { "apiKey": "codex-app-server", ... }
    For CORE anthropic, it writes the env var name as a non-secret marker:
    "anthropic": { "apiKey": "ANTHROPIC_API_KEY", ... }
  6. openclaw secrets audit now reports plaintextCount: 4 on main/models.json (xiaomi value is a real external API key — security regression).
  7. The same will happen on the 4 other agents the next time their models.json is regenerated (e.g. by heartbeat — see #42355).

Expected

For SecretRef-backed providers, the regenerator must persist a non-secret marker (e.g. the env var name, secretref-managed, or a secretref-env:<NAME> form on apiKey paths) — never the resolved value — regardless of whether the provider is in the CORE allowlist. Doc states: "Marker persistence is source-authoritative: OpenClaw writes markers from the active source config snapshot (pre-resolution), not from resolved runtime secret values" (reference/secretref-credential-surface).

Code references (2026.5.18 dist)

  • dist/models-config-hNCfzm-l.js writes models.json (targetPath = path.join(agentDir, "models.json") ~ line 918).
  • dist/model-auth-markers-CiLEGnXI.js has resolveEnvSecretRefHeaderValueMarker(name) = \secretref-env:${name}`but **no equivalentresolveEnvSecretRefApiKeyMarker(name)** for the apiKey field — so the regenerator falls back to the env var name only when it matches listKnownEnvApiKeyMarkers()`, otherwise it appears to inline the resolved value.
  • dist/provider-env-vars-CDIyh39z.js CORE_PROVIDER_AUTH_ENV_VAR_CANDIDATES is the hardcoded allowlist (6 providers only). Custom providers without a manifest contribution fall outside.

Suggested fix

Mirror the header-value path: introduce resolveEnvSecretRefApiKeyMarker(name) = "secretref-env:${name}" (or use secretref-managed like anthropic2/xai do today) and ensure the regenerator emits this marker for any SecretRef-backed apiKey, then teach isNonSecretApiKeyMarker to accept it. This converges with #84376's proposal (add CODEX_APP_SERVER_AUTH_MARKER to isNonSecretApiKeyMarker) but generalizes to all SecretRef-backed apiKeys.

Related

  • Closed regressions: #38757, #42355, #67719
  • Open partial: #84376 (codex sentinel only), #80799 (cross-agent disclosure), #66961 (separate issue on unresolved-provider pollution)

Workaround (current)

For real external secrets, migrate the provider's apiKey out of models.json cache by using openclaw models auth paste-token --provider <id> --agent <id> — this persists in auth-profiles.json (which uses keyRef/SecretRef properly) and lets the regenerator write apiKey: "secretref-managed" (accepted marker). Required for every (agent × custom-provider-with-real-secret) pair — verbose but the only currently safe path.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [Bug][Security][Regression] models status --probe rewrites agent models.json with resolved plaintext apiKey for non-CORE custom providers (2026.5.18)