openclaw - 💡(How to fix) Fix Discord plugin: channels.discord.*.token ref resolution silently fails (plus companion config patch recursive-merge bug)

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…

OpenClaw 2026.5.7 documents openclaw config set channels.discord.token --ref-provider <name> --ref-source env --ref-id <ENV_VAR> as the canonical way to bind the Discord bot token to an environment variable (see openclaw config set --help output, examples block). In practice, this ref structure is silently rejected by the Discord plugin and the bot never attempts a WebSocket connection. No journal lines appear after [gateway] http server listening (... discord ...), and no resolver error is logged.

The workaround is to leave both channels.discord.token and channels.discord.accounts.<id>.token unset entirely; the plugin then falls back to reading process.env.DISCORD_BOT_TOKEN directly (when the account id is DEFAULT_ACCOUNT_ID). This works but contradicts the documented ref-based configuration path and effectively requires the env var to be set in the gateway process, which only works if the gateway's systemd unit is wrapped in a process-level secrets injector like doppler run --.

Error Message

OpenClaw 2026.5.7 documents openclaw config set channels.discord.token --ref-provider <name> --ref-source env --ref-id <ENV_VAR> as the canonical way to bind the Discord bot token to an environment variable (see openclaw config set --help output, examples block). In practice, this ref structure is silently rejected by the Discord plugin and the bot never attempts a WebSocket connection. No journal lines appear after [gateway] http server listening (... discord ...), and no resolver error is logged. 3. Observe journal: gateway boots, plugin loads (http server listening (... discord ...)), but Discord plugin never logs [discord] [default] starting provider. No error appears. The plugin's token-resolution function returns status configured_unavailable and the channel-startup code treats this as "configured but unusable," silently skipping the WebSocket connection attempt. No error log emitted at the channel layer.

  • The "configured_unavailable" branch in resolveDiscordToken returns status only, never logs at warn-level. A [discord] token ref resolution failed: ... warning here would have saved ~30 minutes of investigation. OpenClaw 2026.5.7's openclaw config patch (and openclaw config set --strict-json) reports a misleading validation error when the patch contains an object value with a 19-digit numeric-string key (Discord snowflake guild IDs are 18-19 digits) AND the patch path is recursively merged into existing config. Reproducible: Validator error: channels.discord.guilds: invalid config: must be object. But the value clearly IS an object.

Root Cause

Adding secrets.providers.default = {source: "env"} to the config does not change behavior — this is the key finding (see Root cause below).

Fix Action

Fix / Workaround

The workaround is to leave both channels.discord.token and channels.discord.accounts.<id>.token unset entirely; the plugin then falls back to reading process.env.DISCORD_BOT_TOKEN directly (when the account id is DEFAULT_ACCOUNT_ID). This works but contradicts the documented ref-based configuration path and effectively requires the env var to be set in the gateway process, which only works if the gateway's systemd unit is wrapped in a process-level secrets injector like doppler run --.

Workaround (current)

Bug 2: openclaw config patch recursive-merge mishandles object values with 19-digit-numeric-string keys

Code Example

openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN
   openclaw config set channels.discord.accounts.default.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN

---

function resolveDiscordTokenValue(params) {
    const resolved = resolveSecretInputString({
        value: params.value,
        path: params.path,
        defaults: params.cfg.secrets?.defaults,
        mode: "inspect"
    });
    if (resolved.status === "available") return { ... };
    if (resolved.status === "configured_unavailable") return { status: "configured_unavailable" };
    return { status: "missing" };
}

---

const envToken = accountId === DEFAULT_ACCOUNT_ID
    ? normalizeDiscordToken(opts.envToken ?? process.env.DISCORD_BOT_TOKEN, "DISCORD_BOT_TOKEN")
    : void 0;
if (envToken) return { token: envToken, source: "env", tokenStatus: "available" };

---

openclaw config unset channels.discord.accounts.default.token
   openclaw config unset channels.discord.token

---

[discord] [default] starting provider
[discord] client initialized as <bot-app-id>; awaiting gateway readiness
[discord] [default] Discord bot probe resolved @<botname>

---

{"channels":{"discord":{"groupPolicy":"allowlist","guilds":{"1468687940984115202":{"requireMention":false}},"accounts":{"default":{"groupPolicy":"allowlist"}}}}}

---

openclaw config patch --file /tmp/guilds.json5 --replace-path channels.discord.guilds

---

[discord] channels resolved: 1468687940984115202 (guild:ZeusMoltbot; aliases:guild:1468687940984115202)
RAW_BUFFERClick to expand / collapse

Bug 1: Discord plugin's resolveDiscordToken ignores secrets.providers; documented ref syntax silently fails

Summary

OpenClaw 2026.5.7 documents openclaw config set channels.discord.token --ref-provider <name> --ref-source env --ref-id <ENV_VAR> as the canonical way to bind the Discord bot token to an environment variable (see openclaw config set --help output, examples block). In practice, this ref structure is silently rejected by the Discord plugin and the bot never attempts a WebSocket connection. No journal lines appear after [gateway] http server listening (... discord ...), and no resolver error is logged.

The workaround is to leave both channels.discord.token and channels.discord.accounts.<id>.token unset entirely; the plugin then falls back to reading process.env.DISCORD_BOT_TOKEN directly (when the account id is DEFAULT_ACCOUNT_ID). This works but contradicts the documented ref-based configuration path and effectively requires the env var to be set in the gateway process, which only works if the gateway's systemd unit is wrapped in a process-level secrets injector like doppler run --.

Reproduction

OpenClaw 2026.5.7 on Linux (Ubuntu 24.04). Gateway running under user-systemd, ExecStart wrapped in doppler run --silent --. DISCORD_BOT_TOKEN exposed via Doppler to the gateway process (verified 72-char value present in process.env).

  1. Create an openclaw.json with the channel binding:
    openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN
    openclaw config set channels.discord.accounts.default.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN
  2. Restart the gateway.
  3. Observe journal: gateway boots, plugin loads (http server listening (... discord ...)), but Discord plugin never logs [discord] [default] starting provider. No error appears.

Adding secrets.providers.default = {source: "env"} to the config does not change behavior — this is the key finding (see Root cause below).

Expected behavior

The Discord plugin should resolve the ref through secrets.providers.<name> (and secrets.defaults.env as documented in resolveDefaultSecretProviderAlias), then attempt to connect with the resolved token.

Actual behavior

The plugin's token-resolution function returns status configured_unavailable and the channel-startup code treats this as "configured but unusable," silently skipping the WebSocket connection attempt. No error log emitted at the channel layer.

Root cause (from reading the source)

In OpenClaw 2026.5.7, the Discord plugin's token handling is in @openclaw/discord/dist/token-BZtonk7d.js, function resolveDiscordToken:

function resolveDiscordTokenValue(params) {
    const resolved = resolveSecretInputString({
        value: params.value,
        path: params.path,
        defaults: params.cfg.secrets?.defaults,
        mode: "inspect"
    });
    if (resolved.status === "available") return { ... };
    if (resolved.status === "configured_unavailable") return { status: "configured_unavailable" };
    return { status: "missing" };
}

Two design choices combine to break ref resolution:

  1. mode: "inspect"resolveSecretInputString is called in inspect mode, not resolve mode. This appears to be a check-without-side-effects path that returns availability status rather than actual values.

  2. Only defaults is passed, not providers — the function passes params.cfg.secrets?.defaults, but resolveSecretInputString likely also needs secrets.providers to find configured providers like default. With defaults only, ref resolution cannot find the provider definition.

The core resolver in the openclaw core dist (resolveConfiguredProvider function) requires either config.secrets.providers[ref.provider] to exist OR ref.provider === resolveDefaultSecretProviderAlias(config, "env"). The inspect-mode call from the Discord plugin appears to lack the context to satisfy either path.

The plugin then has a built-in env fallback at the bottom of resolveDiscordToken:

const envToken = accountId === DEFAULT_ACCOUNT_ID
    ? normalizeDiscordToken(opts.envToken ?? process.env.DISCORD_BOT_TOKEN, "DISCORD_BOT_TOKEN")
    : void 0;
if (envToken) return { token: envToken, source: "env", tokenStatus: "available" };

This fallback works correctly. But it ONLY fires when both the account-level and channel-level token fields are unset; if either is set as a ref, the configured_unavailable short-circuit returns before reaching the fallback.

Suggested fixes (one of)

A. Have the Discord plugin's resolveDiscordTokenValue pass the entire cfg.secrets (or at least cfg.secrets?.providers in addition to defaults) so the resolver can find configured providers.

B. Change the inspect-mode short-circuit so configured_unavailable only fires after the env fallback has been attempted, not before. The current order is "configured but can't resolve → bail" which prevents the documented env path from running.

C. Update openclaw config set --help to mark channels.discord.*.token as an unsupported ref target, and document the env-fallback as the canonical path: "to use Doppler/env injection for the Discord bot token, leave channels.discord.*.token unset and ensure DISCORD_BOT_TOKEN is in the gateway process env."

Option A is the cleanest from a user-of-refs perspective. Option B is the most backwards-compatible. Option C is the lowest-cost but locks the env-fallback in as the documented path.

Environment

  • OpenClaw 2026.5.7 (eeef486)
  • Plugin: @openclaw/discord
  • Linux (Ubuntu 24.04.4 LTS, kernel 6.8.0-111-generic)
  • Gateway under user-systemd, ExecStart wrapped in doppler run --silent --

Workaround (current)

  1. Ensure DISCORD_BOT_TOKEN is present in the gateway process env at runtime (via doppler, sops, or whatever secrets injector you use).
  2. Unset both Discord token fields:
    openclaw config unset channels.discord.accounts.default.token
    openclaw config unset channels.discord.token
  3. Restart the gateway. Plugin's env fallback at the bottom of resolveDiscordToken takes over.

Verified successful via journal log signature:

[discord] [default] starting provider
[discord] client initialized as <bot-app-id>; awaiting gateway readiness
[discord] [default] Discord bot probe resolved @<botname>

Related observations

  • Discord intents schema (channels.discord.intents and channels.discord.accounts.<id>.intents) only accepts presence, guildMembers, voiceStates as properties. messageContent is NOT a configurable intent in the OpenClaw schema — the plugin always requests it. This is fine in practice (Discord Dev Portal toggle is the actual gate) but the schema rejection of messageContent is surprising given that some prior guidance suggested setting channels.discord.accounts.default.intents.messageContent = true. The schema rejection message was clear (additional properties), but the documentation should probably note that messageContent is always requested.

  • The "configured_unavailable" branch in resolveDiscordToken returns status only, never logs at warn-level. A [discord] token ref resolution failed: ... warning here would have saved ~30 minutes of investigation.


Bug 2: openclaw config patch recursive-merge mishandles object values with 19-digit-numeric-string keys

Initially misdiagnosed as a JSON5 parser bug. The actual bug is in the patch tool's recursive-merge logic. Confirmed by: openclaw config patch --replace-path channels.discord.guilds (which forces full replacement instead of recursive merge) VALIDATES the same patch that fails without the flag.

OpenClaw 2026.5.7's openclaw config patch (and openclaw config set --strict-json) reports a misleading validation error when the patch contains an object value with a 19-digit numeric-string key (Discord snowflake guild IDs are 18-19 digits) AND the patch path is recursively merged into existing config. Reproducible:

Fails:

{"channels":{"discord":{"groupPolicy":"allowlist","guilds":{"1468687940984115202":{"requireMention":false}},"accounts":{"default":{"groupPolicy":"allowlist"}}}}}

Validator error: channels.discord.guilds: invalid config: must be object. But the value clearly IS an object.

Validates fine: same patch with "testkey" instead of "1468687940984115202".

The 19-digit numeric-string key is the only variable. JavaScript's Number type can safely represent integers only up to 2^53-1 (= 9007199254740991, 16 digits). The 19-digit Discord guild ID exceeds this. The recursive merger appears to coerce the key into a Number somewhere, then misclassify the resulting structure.

Suggested fix

  • Audit the recursive-merge code path in openclaw config patch. Ensure object keys are preserved as strings throughout the merge pipeline regardless of whether they look numeric.
  • Test case to add: a config that includes a Discord guild ID like 1468687940984115202 as an object key under channels.discord.guilds. Should validate without --replace-path.

Workaround — VERIFIED WORKING

Use openclaw config patch --replace-path <path> --file <patch> instead of bare openclaw config patch --file <patch>. The --replace-path flag forces the path to be REPLACED instead of recursively merged, bypassing the bug.

Example that previously failed with "must be object" but works with --replace-path:

openclaw config patch --file /tmp/guilds.json5 --replace-path channels.discord.guilds

After applying this with a Zeus guild ID 1468687940984115202, the journal showed:

[discord] channels resolved: 1468687940984115202 (guild:ZeusMoltbot; aliases:guild:1468687940984115202)

Confirming the patch landed correctly AND the runtime can read the 19-digit key fine. The JSON5 parser is innocent; the recursive merger is the bug.

Environment

Same as Bug 1 above.

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…

FAQ

Expected behavior

The Discord plugin should resolve the ref through secrets.providers.<name> (and secrets.defaults.env as documented in resolveDefaultSecretProviderAlias), then attempt to connect with the resolved token.

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 Discord plugin: channels.discord.*.token ref resolution silently fails (plus companion config patch recursive-merge bug)