openclaw - 💡(How to fix) Fix ${ENV_VAR} refs in plugins.entries.*.config not resolved before passing to plugin (regression in 2026.5.28-beta.4)

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…

In 2026.5.28-beta.4, ${ENV_VAR} references inside plugin config (plugins.entries.<id>.config.*) are handed to the plugin unresolved — the plugin receives the literal/unsubstituted value instead of the env var's value. The same ${ENV_VAR} references in provider config (models.providers.*) still resolve correctly.

For any install that uses ${VAR} refs to inject secrets/URLs into a plugin's config (a documented, common pattern), the affected plugin silently receives a bad value. In our case a provider-plugin's API key arrived unsubstituted, so every upstream request was rejected and the agent silently failed over to a fallback model — the user appeared to be talking to their configured model while actually running on the fallback, with no error surfaced in chat.

Error Message

For any install that uses ${VAR} refs to inject secrets/URLs into a plugin's config (a documented, common pattern), the affected plugin silently receives a bad value. In our case a provider-plugin's API key arrived unsubstituted, so every upstream request was rejected and the agent silently failed over to a fallback model — the user appeared to be talking to their configured model while actually running on the fallback, with no error surfaced in chat.

Root Cause

Root cause (analysis)

Fix Action

Workaround

Replace ${VAR} refs in plugin config with the literal value (requires a gateway restart so the plugin re-reads config).


cc @Peetiegonzalez

Environment: macOS 15.7.3 (Intel), node 25.9.0, OpenClaw 2026.5.28-beta.4. Investigated with AI assistance.

Code Example

"plugins": { "entries": { "myplugin": { "enabled": true,
     "config": { "apiKey": "${MY_PLUGIN_KEY}" } } } }
   "env": { "vars": { "MY_PLUGIN_KEY": "<real-key>" } }
RAW_BUFFERClick to expand / collapse

${ENV_VAR} references in plugins.entries.*.config are not resolved before being passed to the plugin (regression in 2026.5.28-beta.4)

Summary

In 2026.5.28-beta.4, ${ENV_VAR} references inside plugin config (plugins.entries.<id>.config.*) are handed to the plugin unresolved — the plugin receives the literal/unsubstituted value instead of the env var's value. The same ${ENV_VAR} references in provider config (models.providers.*) still resolve correctly.

For any install that uses ${VAR} refs to inject secrets/URLs into a plugin's config (a documented, common pattern), the affected plugin silently receives a bad value. In our case a provider-plugin's API key arrived unsubstituted, so every upstream request was rejected and the agent silently failed over to a fallback model — the user appeared to be talking to their configured model while actually running on the fallback, with no error surfaced in chat.

Affected / regression

  • Broken: 2026.5.28-beta.4
  • Working: 2026.5.27 (same config, same ${VAR} plugin entry — worked before upgrading)
  • Suspect range: the plugin-startup/config perf changes between the two tags, e.g. perf(agent): skip plugin validation for gateway dispatch, perf(config): prefer native JSON parsing, and the plugin-metadata snapshot/memoization path. (Not bisected to a single commit.)

Reproduction

  1. A provider plugin whose config takes an API key, e.g.:
    "plugins": { "entries": { "myplugin": { "enabled": true,
      "config": { "apiKey": "${MY_PLUGIN_KEY}" } } } }
    "env": { "vars": { "MY_PLUGIN_KEY": "<real-key>" } }
  2. Run a turn that uses the plugin.
  3. The plugin authenticates with the literal ${MY_PLUGIN_KEY} (unresolved) and the upstream rejects it.
  4. Replace "${MY_PLUGIN_KEY}" with the real key value inline → works immediately.

Evidence

  • A/B (decisive): config.apiKey: "${MY_PLUGIN_KEY}" → every plugin request rejected (embedded_run_agent_end … provider=<plugin> isError:true, upstream 403). Same config with the literal key inline → 200, works. Toggled via gateway restarts, reproduced repeatedly.
  • Provider vs plugin asymmetry (rules out "var unset"): the same ${VAR} reference resolves correctly for a provider entry (openclaw models status auth overview shows effective=env:<value> | source=env: <VAR>), but not for the plugin entry. The env var is present in process.env.
  • The upstream endpoint, the key, both transports, and the HTTP client all work in isolation (curl + node/undici, 200) — the failure only occurs through the plugin via the gateway.

Root cause (analysis)

  • resolveConfigEnvVars / substituteAny (src/config/env-substitution.ts) deep-walks the entire config uniformly with no plugins skip — so substitution itself is correct, and the normal resolveConfigForRead path resolves plugin config too.
  • Therefore the defect is that the plugin loader consumes a config representation that bypassed resolveConfigForRead — i.e. the sourceConfig (raw, pre-substitution) vs runtimeConfig (resolved) split in the config-snapshot / plugin-metadata machinery. The plugin loader reads normalizePluginsConfig(cfg.plugins) from options.config; whichever caller (gateway boot / loadConfig({ skipPluginValidation }) / memoized plugin-metadata snapshot) supplies that cfg appears to be supplying the raw snapshot for plugin loading. (Exact caller/line not yet pinned.)

Impact

  • Any install using ${VAR} in plugins.entries.*.config (a documented pattern) → the plugin silently gets a broken value. Most users will see only "provider/feature down" with no indication why.
  • Worsened by silent fallback (arguably a second, separable issue): a complete provider outage produced no in-chat fallback notice and /status showed the fallback model with no "not your default" marker — so the misconfiguration is invisible. (Happy to file separately.)

What I could not confirm

  • The exact bytes the plugin emits (literal ${VAR} vs empty) — capture rig (PIONEER_BASE_URL redirect) didn't land because the var wasn't inherited by the gateway process. Not needed to establish the defect; the A/B + provider/plugin asymmetry are sufficient.

Workaround

Replace ${VAR} refs in plugin config with the literal value (requires a gateway restart so the plugin re-reads config).


cc @Peetiegonzalez

Environment: macOS 15.7.3 (Intel), node 25.9.0, OpenClaw 2026.5.28-beta.4. Investigated with AI assistance.

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 ${ENV_VAR} refs in plugins.entries.*.config not resolved before passing to plugin (regression in 2026.5.28-beta.4)