openclaw - ✅(Solved) Fix Bug: Slack plugin eagerly resolves SecretRef tokens at register time, crashing CLI for all agents/* commands [3 pull requests, 1 comments, 2 participants]

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…
GitHub stats
openclaw/openclaw#63937Fetched 2026-04-10 03:41:33
View on GitHub
Comments
1
Participants
2
Timeline
3
Reactions
0
Author
Participants
Timeline (top)
commented ×1cross-referenced ×1referenced ×1

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.

Error Message

[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: ...

Root Cause

The call chain that causes the crash:

  1. command-execution-startup.js — the agents command path sets loadPlugins: "always", so every openclaw agents … invocation force-loads every plugin including Slack.
  2. runtime-registry-loader.js — CLI plugin loading runs with throwOnLoadError: true, making Slack's register-phase throw fatal.
  3. extensions/slack/index.jsregisterSlackPluginHttpRoutes — calls resolveSlackAccount({cfg, accountId}) eagerly while registering HTTP routes.
  4. accounts.jsresolveSlackBotToken(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.

Fix Action

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).

PR fix notes

PR #63944: fix(slack): defer token resolution on plugin register path

Description (problem / solution / changelog)

Summary

registerSlackPluginHttpRoutes() in extensions/slack/src/http/plugin-routes.ts used to call resolveSlackAccount() on every configured account for the sole purpose of reading config.webhookPath. resolveSlackAccount() eagerly runs resolveSlackBotToken/AppToken/UserToken on the merged config (extensions/slack/src/accounts.ts:61-72). When any of those tokens are stored as a SecretRef object — the default for file:local-provided secrets — token resolution throws `"unresolved SecretRef"` and crashes the register path.

The CLI runs plugin registration with throwOnLoadError: true, so every openclaw agents ... subcommand (which forces loadPlugins: \"always\") fails with a fatal PluginLoadFailureError before any agent command can execute. The running gateway process is unaffected because it holds a resolved in-memory runtime snapshot, but the CLI is a separate process that reads raw openclaw.json and has no access to that snapshot.

Fixes #63937

Reproducer (from the issue)

{
  \"channels\": {
    \"slack\": {
      \"accounts\": {
        \"default\": {
          \"botToken\": { \"source\": \"file\", \"provider\": \"local\", \"id\": \"/SLACK_BOT_TOKEN\" },
          \"appToken\": { \"source\": \"file\", \"provider\": \"local\", \"id\": \"/SLACK_APP_TOKEN\" }
        }
      }
    }
  }
}
$ openclaw agents list
[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: ...

Every workaround the reporter tried fails:

  • SLACK_BOT_TOKEN env var → accounts.js evaluates the SecretRef from config before the env fallback
  • channels.slack.enabled: falseregisterSlackPluginHttpRoutes iterates DEFAULT_ACCOUNT_ID unconditionally
  • Removing \"slack\" from plugins.allow → also triggered by the channels.slack config block
  • OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 → invalidates the entire config

Fix

Swap resolveSlackAccount(...).config.webhookPath for mergeSlackAccountConfig(...).webhookPath. mergeSlackAccountConfig() is the already-exported token-free surface — it's what resolveSlackAccount() itself calls internally (accounts.ts:54), without the subsequent token-resolution steps.

The merge helper is already used idiomatically throughout the Slack extension for exactly this pattern:

  • extensions/slack/src/group-policy.ts:23
  • extensions/slack/src/directory-config.ts:15
  • extensions/slack/src/account-inspect.ts:77

Token resolution continues to happen at request time inside handleSlackHttpRequest, which runs under the gateway runtime where secrets are resolvable. No change to the request path, no change to token storage, no change to the outbound send path.

Diff sketch

```diff

  • import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
  • import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
  • import { listSlackAccountIds, resolveSlackAccount } from "../accounts.js";
  • import { listSlackAccountIds, mergeSlackAccountConfig } from "../accounts.js"; ... export function registerSlackPluginHttpRoutes(api: OpenClawPluginApi): void { const accountIds = new Set<string>([DEFAULT_ACCOUNT_ID, ...listSlackAccountIds(api.config)]); const registeredPaths = new Set<string>();
  • for (const accountId of accountIds) {
  • const account = resolveSlackAccount({ cfg: api.config, accountId });
  • registeredPaths.add(normalizeSlackWebhookPath(account.config.webhookPath));
  • for (const rawAccountId of accountIds) {
  • const accountId = normalizeAccountId(rawAccountId);
  • const merged = mergeSlackAccountConfig(api.config, accountId);
  • registeredPaths.add(normalizeSlackWebhookPath(merged.webhookPath));
    } ```

The explicit normalizeAccountId call preserves the normalization that resolveSlackAccount used to apply internally (accounts.ts:50-52).

Tests

Added extensions/slack/src/http/plugin-routes.test.ts with 4 regression cases:

  1. Empty config still registers the default /slack/events path (smoke test)
  2. SecretRef tokens in config do NOT throw during registration (the #63937 regression guard — would fail on main before this patch)
  3. Custom webhookPath from config is honored without touching tokens
  4. Duplicate paths across accounts are deduplicated

All tests use a plain mock OpenClawPluginApi with registerHttpRoute: vi.fn(), no real gateway runtime needed. Validates that register-time token resolution is gone.

Scope

  • Files: extensions/slack/src/http/plugin-routes.ts (-3 +8), extensions/slack/src/http/plugin-routes.test.ts (+103)
  • Production LOC: ~5 real lines changed
  • No changes to accounts.ts, token.js, request path, send path, or any upstream config schema
  • oxlint clean

cc @Takhoffman — Slack channel ownership. Credit to @oliviercp for the complete call-chain RCA in #63937.

Changed files

  • extensions/slack/src/http/plugin-routes.test.ts (added, +103/-0)
  • extensions/slack/src/http/plugin-routes.ts (modified, +14/-5)

PR #65378: fix(slack): forward resolved token in all action call sites

Description (problem / solution / changelog)

In the default single-account SecretRef deployment, buildActionOpts() returned undefined — causing 12+ downstream action sites (sendMessage, editMessage, readMessages, react, pin/unpin, uploadFile, memberInfo, emojiList) to call resolveToken()loadConfig() which crashes on SecretRef objects.

#62097 fixed this for downloadFile only with a site-specific workaround. This completes the pattern structurally: buildActionOpts now always returns an object with the resolved token, eliminating the undefined return path entirely. The site-specific downloadFile workaround is removed (now redundant).

Multi-account and token-override behavior is preserved — accountId and custom tokens are still forwarded when configured.

Refs #57272, #63937 Follow-up to #62097

Changed files

  • extensions/slack/src/action-runtime.test.ts (modified, +89/-36)
  • extensions/slack/src/action-runtime.ts (modified, +1/-7)

PR #66818: fix(secrets): align SecretRef inspect/strict behavior across preload/runtime paths

Description (problem / solution / changelog)

Summary

  • Problem: SecretRef handling was inconsistent across plugin preload, registration/setup surfaces, and runtime auth/tool paths. Some read-only flows could fail early on unresolved refs.
  • Why it matters: non-runtime commands and status paths should stay resilient when config contains unresolved SecretRefs, while runtime should remain strict where credentials must actually resolve.
  • What changed:
    • Added a shared inspect/strict SecretRef resolution contract in src/config/types.secrets.ts, exported via src/plugin-sdk/secret-input.ts.
    • Routed status deferred plugin preload through explicit resolved/source snapshots (src/cli/plugin-registry-loader.ts, src/commands/status.scan.fast-json.ts).
    • Updated read-only provider status resolution to prefer inspectAccount and skip throwing accounts (src/commands/agents.providers.ts).
    • Removed credential resolution from Slack route registration (extensions/slack/src/http/plugin-routes.ts).
    • Normalized provider/tool SecretRef handling for custom provider auth, xAI tool auth, and Firecrawl config paths.
    • Added Exa web-search SecretRef target coverage and synced docs matrix/surface.
    • Updated Telegram runtime token resolution to support env SecretRefs while preserving strict behavior for unresolved non-env refs.
  • What did NOT change (scope boundary):
    • No unrelated channel/runtime behavior refactors outside SecretRef lifecycle and snapshot-threading surfaces.
    • No protocol or command-surface expansion.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #63937
  • Closes #57272
  • Closes #57684
  • Closes #64955
  • Closes #65510
  • Supersedes #49805
  • Supersedes #65833
  • Supersedes #56784
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: SecretRef reads were happening at multiple lifecycle points without one shared contract for read-only inspection vs strict runtime resolution.
  • Missing detection / guardrail: registration/read-only flows were not consistently guarded against unresolved refs, and status preload did not always reuse explicit resolved/source snapshots.
  • Contributing context (if known): plugin/channel/provider codepaths evolved with local helpers, causing divergence in when unresolved refs throw vs degrade gracefully.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • src/config/types.secrets.resolution.test.ts
    • src/cli/plugin-registry-loader.test.ts
    • src/commands/status.scan.test.ts
    • src/commands/agents.providers.test.ts
    • extensions/slack/src/http/plugin-routes.test.ts
    • src/agents/model-auth.test.ts
    • extensions/xai/src/tool-auth-shared.test.ts
    • extensions/firecrawl/src/firecrawl-tools.test.ts
    • src/secrets/target-registry.test.ts
    • src/cli/command-secret-targets.test.ts
    • extensions/telegram/src/token.test.ts
  • Scenario the test should lock in:
    • Read-only paths do not hard-fail on unresolved SecretRefs.
    • Runtime paths remain strict where non-env refs are unresolved.
    • Deferred plugin preload receives explicit resolved/source snapshots.
    • Secret target registry/docs stay in sync.
  • Why this is the smallest reliable guardrail:
    • These seams are exactly where resolution mode and snapshot threading decisions are made.
  • Existing test that already covers this (if any):
    • Existing status/agents/provider/channel tests were extended at those boundaries.
  • If no new test is added, why not:
    • N/A

User-visible / Behavior Changes

  • openclaw agents list and other read-only/status paths are more resilient when unresolved SecretRefs exist in channel/provider configs.
  • Slack route registration no longer resolves credential refs at registration time.
  • Status deferred plugin preload uses explicit resolved/source config snapshots.
  • Custom provider/xAI/Firecrawl SecretRef handling now treats env refs and unresolved refs consistently for runtime-safe behavior.
  • Exa web-search SecretRef target is now included in secret target registry/docs.
  • Telegram runtime can resolve env-backed SecretRefs for bot tokens while retaining strict handling for unresolved non-env refs.

Diagram (if applicable)

Before:
[CLI/read-only preload] -> [strict secret read in mixed paths] -> [throws early on unresolved ref]

After:
[CLI/read-only preload] -> [inspect mode + resolved/source snapshot threading] -> [degrades safely]
[runtime send/start] -> [strict resolution at execution boundary] -> [throws only when truly required]

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (Yes)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation:
    • Risk: accidentally making runtime secret resolution too permissive.
    • Mitigation: strict mode is preserved for runtime-required non-env refs; inspect mode is explicitly scoped to read-only/setup/preload surfaces.

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22+, pnpm workspace
  • Model/provider: N/A
  • Integration/channel (if any): Slack, Telegram, xAI, Firecrawl, model providers
  • Relevant config (redacted): configs containing structured SecretRefs (env + non-env)

Steps

  1. Add unresolved SecretRef values to affected config paths.
  2. Run read-only/status commands and plugin preload flows.
  3. Run runtime credential paths for affected providers/channels.

Expected

  • Read-only/status flows do not crash on unresolved refs.
  • Runtime env refs resolve from environment where applicable.
  • Runtime unresolved non-env refs remain strict failures.

Actual

  • Matches expected in updated tests and targeted command-path verification.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • Built project successfully (pnpm build).
    • Verified Plugin SDK API drift workflow (pnpm plugin-sdk:api:check, pnpm plugin-sdk:api:gen, re-check pass).
    • Ran targeted tests covering config resolver, status preload, provider auth/tool paths, Slack route registration, Exa target/docs sync, and Telegram token resolution.
  • Edge cases checked:
    • Env SecretRef availability vs missing env var.
    • Unresolved non-env SecretRef behavior in runtime paths.
    • Read-only surfaces with inspect-first handling.
  • What you did not verify:
    • Full repository pnpm test did not finish green due unrelated failing shards outside this PR surface.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps:
    • N/A

Risks and Mitigations

  • Risk:

    • Inspect-mode use could expand unintentionally into runtime paths.
    • Mitigation:
      • Strict mode remains default for runtime helper paths; tests assert strict behavior for unresolved non-env refs.
  • Risk:

    • Secret target registry changes can drift from docs.
    • Mitigation:
      • Updated matrix + credential surface docs and docs-sync tests.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • docs/.generated/plugin-sdk-api-baseline.sha256 (modified, +2/-2)
  • docs/reference/secretref-credential-surface.md (modified, +1/-0)
  • docs/reference/secretref-user-supplied-credentials-matrix.json (modified, +7/-0)
  • extensions/firecrawl/src/config.ts (modified, +92/-25)
  • extensions/firecrawl/src/firecrawl-tools.test.ts (modified, +131/-0)
  • extensions/slack/src/http/plugin-routes.test.ts (added, +62/-0)
  • extensions/slack/src/http/plugin-routes.ts (modified, +4/-3)
  • extensions/telegram/src/token.test.ts (modified, +144/-1)
  • extensions/telegram/src/token.ts (modified, +96/-7)
  • extensions/xai/src/tool-auth-shared.test.ts (modified, +146/-0)
  • extensions/xai/src/tool-auth-shared.ts (modified, +121/-13)
  • src/agents/model-auth.test.ts (modified, +201/-0)
  • src/agents/model-auth.ts (modified, +64/-1)
  • src/cli/command-secret-targets.test.ts (modified, +2/-0)
  • src/cli/command-secret-targets.ts (modified, +1/-0)
  • src/cli/plugin-registry-loader.test.ts (modified, +17/-0)
  • src/cli/plugin-registry-loader.ts (modified, +10/-1)
  • src/commands/agents.providers.test.ts (added, +124/-0)
  • src/commands/agents.providers.ts (modified, +37/-1)
  • src/commands/status.scan.fast-json.test.ts (modified, +45/-0)
  • src/commands/status.scan.fast-json.ts (modified, +2/-0)
  • src/commands/status.scan.test.ts (modified, +18/-6)
  • src/config/types.secrets.resolution.test.ts (added, +80/-0)
  • src/config/types.secrets.ts (modified, +55/-7)
  • src/plugin-sdk/secret-input.ts (modified, +7/-1)
  • src/secrets/target-registry-data.ts (modified, +11/-0)
  • src/secrets/target-registry.test.ts (modified, +14/-0)

Code Example

"channels": {
     "slack": {
       "accounts": {
         "default": {
           "botToken": { "source": "file", "provider": "local", "id": "/SLACK_BOT_TOKEN" },
           "appToken": { "source": "file", "provider": "local", "id": "/SLACK_APP_TOKEN" }
         }
       }
     }
   }

---

openclaw agents add myagent --workspace ~/.openclaw/workspace-myagent
   openclaw agents list

---

[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: ...
RAW_BUFFERClick to expand / collapse

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

  1. 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" }
          }
        }
      }
    }
  2. 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:

  1. command-execution-startup.js — the agents command path sets loadPlugins: "always", so every openclaw agents … invocation force-loads every plugin including Slack.
  2. runtime-registry-loader.js — CLI plugin loading runs with throwOnLoadError: true, making Slack's register-phase throw fatal.
  3. extensions/slack/index.jsregisterSlackPluginHttpRoutes — calls resolveSlackAccount({cfg, accountId}) eagerly while registering HTTP routes.
  4. accounts.jsresolveSlackBotToken(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 workaroundWhy it fails
Set SLACK_BOT_TOKEN env varaccounts.js evaluates the SecretRef from config before the env fallback; throws before checking env
Set channels.slack.enabled: falseregisterSlackPluginHttpRoutes iterates DEFAULT_ACCOUNT_ID unconditionally regardless of enabled
Remove "slack" from plugins.allowThe plugin is also triggered by the channels.slack config block; removing from allowlist alone is insufficient
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1Invalidates 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

extent analysis

TL;DR

Temporarily replace SecretRef objects in openclaw.json with dummy string values to allow CLI commands to execute without fatal errors.

Guidance

  • Identify the SecretRef objects in openclaw.json that are causing the issue, specifically botToken and appToken for the Slack channel.
  • Replace these SecretRef objects with temporary dummy string values to bypass the PluginLoadFailureError.
  • Run the desired CLI command (e.g., openclaw agents add or openclaw agents list) with the modified openclaw.json.
  • After the command executes, restore the original SecretRef objects in openclaw.json to maintain the intended secret management.

Example

// Temporary modification to openclaw.json
"channels": {
  "slack": {
    "accounts": {
      "default": {
        "botToken": "dummy-bot-token",
        "appToken": "dummy-app-token"
      }
    }
  }
}

Notes

This workaround does not address the underlying issue but allows for temporary execution of CLI commands. The proposed fix involves modifying the Slack plugin's register hook to defer token reads until an inbound Slack event is processed.

Recommendation

Apply the workaround by temporarily replacing SecretRef objects with dummy string values, as this allows for immediate execution of CLI commands without requiring changes to the Slack plugin or OpenClaw configuration.

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