Summary
Any CLI command under openclaw agents (e.g. agents add, agents list) fails with a fatal PluginLoadFailureError when the Slack channel's botToken or appToken are stored as SecretRef objects (the default when secrets are configured via the file:local provider). The running gateway process is unaffected because it holds a resolved in-memory snapshot, but the CLI is a separate process that reads raw openclaw.json and crashes before the command can execute.
Steps to Reproduce
- Configure a Slack channel account with token secrets stored as
SecretRef (e.g. file:local:/SLACK_BOT_TOKEN):
"channels": {
"slack": {
"accounts": {
"default": {
"botToken": { "source": "file", "provider": "local", "id": "/SLACK_BOT_TOKEN" },
"appToken": { "source": "file", "provider": "local", "id": "/SLACK_APP_TOKEN" }
}
}
}
}
- Run any
agents subcommand:
openclaw agents add myagent --workspace ~/.openclaw/workspace-myagent
openclaw agents list
Observed Behavior
[plugins] slack failed during register from .../extensions/slack/index.js:
Error: channels.slack.accounts.default.botToken: unresolved SecretRef "file:local:/SLACK_BOT_TOKEN".
Resolve this command against an active gateway runtime snapshot before reading it.
[openclaw] Failed to start CLI: PluginLoadFailureError: plugin load failed: slack: ...
The process exits with code 1. No agent command can run.
Expected Behavior
The Slack plugin's register phase should not attempt to resolve tokens. Token resolution should be deferred to the actual request handler (i.e. when a Slack event arrives), not at plugin registration time. The CLI should be able to execute agents commands regardless of whether channel secrets are resolvable.
Root Cause Analysis
The call chain that causes the crash:
command-execution-startup.js — the agents command path sets loadPlugins: "always", so every openclaw agents … invocation force-loads every plugin including Slack.
runtime-registry-loader.js — CLI plugin loading runs with throwOnLoadError: true, making Slack's register-phase throw fatal.
extensions/slack/index.js → registerSlackPluginHttpRoutes — calls resolveSlackAccount({cfg, accountId}) eagerly while registering HTTP routes.
accounts.js → resolveSlackBotToken(merged.botToken, …) → normalizeResolvedSecretInputString — if merged.botToken is still a SecretRef object (not yet resolved), throws the "unresolved SecretRef" error.
The token is read from raw openclaw.json by the CLI, which has no access to the gateway's runtime secret-resolution snapshot.
Why Existing Escape Hatches Don't Work
| Attempted workaround |
Why it fails |
Set SLACK_BOT_TOKEN env var |
accounts.js evaluates the SecretRef from config before the env fallback; throws before checking env |
Set channels.slack.enabled: false |
registerSlackPluginHttpRoutes iterates DEFAULT_ACCOUNT_ID unconditionally regardless of enabled |
Remove "slack" from plugins.allow |
The plugin is also triggered by the channels.slack config block; removing from allowlist alone is insufficient |
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 |
Invalidates the entire config (channels.slack: unknown channel id) |
Workaround
Temporarily replace the SecretRef objects in openclaw.json with dummy string values, run the CLI command, then restore the original SecretRef objects. The running gateway is unaffected (it holds its own in-memory resolved state and does not watch openclaw.json).
Proposed Fix
In the Slack plugin's register hook, defer all token reads to the actual request handler. The route path should be registered unconditionally; resolveSlackAccount should only be called when an inbound Slack event is being processed, at which point the gateway runtime snapshot is available for secret resolution.
Environment
- OpenClaw version: 2026.4.9 (0512059)
- Platform: macOS (darwin 24.6.0)
- Secret provider:
file:local
- Command that triggered the error:
openclaw agents add dasher --workspace ~/.openclaw/workspace-dasher
Summary
Any CLI command under
openclaw agents(e.g.agents add,agents list) fails with a fatalPluginLoadFailureErrorwhen the Slack channel'sbotTokenorappTokenare stored asSecretRefobjects (the default when secrets are configured via thefile:localprovider). The running gateway process is unaffected because it holds a resolved in-memory snapshot, but the CLI is a separate process that reads rawopenclaw.jsonand crashes before the command can execute.Steps to Reproduce
SecretRef(e.g.file:local:/SLACK_BOT_TOKEN):agentssubcommand:Observed Behavior
The process exits with code 1. No agent command can run.
Expected Behavior
The Slack plugin's register phase should not attempt to resolve tokens. Token resolution should be deferred to the actual request handler (i.e. when a Slack event arrives), not at plugin registration time. The CLI should be able to execute
agentscommands regardless of whether channel secrets are resolvable.Root Cause Analysis
The call chain that causes the crash:
command-execution-startup.js— theagentscommand path setsloadPlugins: "always", so everyopenclaw agents …invocation force-loads every plugin including Slack.runtime-registry-loader.js— CLI plugin loading runs withthrowOnLoadError: true, making Slack's register-phase throw fatal.extensions/slack/index.js→registerSlackPluginHttpRoutes— callsresolveSlackAccount({cfg, accountId})eagerly while registering HTTP routes.accounts.js→resolveSlackBotToken(merged.botToken, …)→normalizeResolvedSecretInputString— ifmerged.botTokenis still aSecretRefobject (not yet resolved), throws the "unresolved SecretRef" error.The token is read from raw
openclaw.jsonby the CLI, which has no access to the gateway's runtime secret-resolution snapshot.Why Existing Escape Hatches Don't Work
SLACK_BOT_TOKENenv varaccounts.jsevaluates theSecretReffrom config before the env fallback; throws before checking envchannels.slack.enabled: falseregisterSlackPluginHttpRoutesiteratesDEFAULT_ACCOUNT_IDunconditionally regardless ofenabled"slack"fromplugins.allowchannels.slackconfig block; removing from allowlist alone is insufficientOPENCLAW_DISABLE_BUNDLED_PLUGINS=1channels.slack: unknown channel id)Workaround
Temporarily replace the
SecretRefobjects inopenclaw.jsonwith dummy string values, run the CLI command, then restore the originalSecretRefobjects. The running gateway is unaffected (it holds its own in-memory resolved state and does not watchopenclaw.json).Proposed Fix
In the Slack plugin's register hook, defer all token reads to the actual request handler. The route path should be registered unconditionally;
resolveSlackAccountshould only be called when an inbound Slack event is being processed, at which point the gateway runtime snapshot is available for secret resolution.Environment
file:localopenclaw agents add dasher --workspace ~/.openclaw/workspace-dasher