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).
- 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
- Restart the gateway.
- 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:
-
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.
-
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)
- Ensure
DISCORD_BOT_TOKEN is present in the gateway process env at runtime (via doppler, sops, or whatever secrets injector you use).
- Unset both Discord token fields:
openclaw config unset channels.discord.accounts.default.token
openclaw config unset channels.discord.token
- 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.
Bug 1: Discord plugin's
resolveDiscordTokenignoressecrets.providers; documented ref syntax silently failsSummary
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 (seeopenclaw config set --helpoutput, 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.tokenandchannels.discord.accounts.<id>.tokenunset entirely; the plugin then falls back to readingprocess.env.DISCORD_BOT_TOKENdirectly (when the account id isDEFAULT_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 likedoppler run --.Reproduction
OpenClaw 2026.5.7 on Linux (Ubuntu 24.04). Gateway running under user-systemd, ExecStart wrapped in
doppler run --silent --.DISCORD_BOT_TOKENexposed via Doppler to the gateway process (verified 72-char value present inprocess.env).openclaw.jsonwith the channel binding: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>(andsecrets.defaults.envas documented inresolveDefaultSecretProviderAlias), then attempt to connect with the resolved token.Actual behavior
The plugin's token-resolution function returns status
configured_unavailableand 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, functionresolveDiscordToken:Two design choices combine to break ref resolution:
mode: "inspect"—resolveSecretInputStringis 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.Only
defaultsis passed, notproviders— the function passesparams.cfg.secrets?.defaults, butresolveSecretInputStringlikely also needssecrets.providersto find configured providers likedefault. Withdefaultsonly, ref resolution cannot find the provider definition.The core resolver in the openclaw core dist (
resolveConfiguredProviderfunction) requires eitherconfig.secrets.providers[ref.provider]to exist ORref.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: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_unavailableshort-circuit returns before reaching the fallback.Suggested fixes (one of)
A. Have the Discord plugin's
resolveDiscordTokenValuepass the entirecfg.secrets(or at leastcfg.secrets?.providersin addition todefaults) so the resolver can find configured providers.B. Change the inspect-mode short-circuit so
configured_unavailableonly 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 --helpto markchannels.discord.*.tokenas an unsupported ref target, and document the env-fallback as the canonical path: "to use Doppler/env injection for the Discord bot token, leavechannels.discord.*.tokenunset and ensureDISCORD_BOT_TOKENis 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/discorddoppler run --silent --Workaround (current)
DISCORD_BOT_TOKENis present in the gateway process env at runtime (via doppler, sops, or whatever secrets injector you use).resolveDiscordTokentakes over.Verified successful via journal log signature:
Related observations
Discord intents schema (
channels.discord.intentsandchannels.discord.accounts.<id>.intents) only acceptspresence,guildMembers,voiceStatesas properties.messageContentis 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 ofmessageContentis surprising given that some prior guidance suggested settingchannels.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
resolveDiscordTokenreturns 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 patchrecursive-merge mishandles object values with 19-digit-numeric-string keysInitially 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(andopenclaw 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:
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
openclaw config patch. Ensure object keys are preserved as strings throughout the merge pipeline regardless of whether they look numeric.1468687940984115202as an object key underchannels.discord.guilds. Should validate without--replace-path.Workaround — VERIFIED WORKING
Use
openclaw config patch --replace-path <path> --file <patch>instead of bareopenclaw config patch --file <patch>. The--replace-pathflag 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:After applying this with a Zeus guild ID
1468687940984115202, the journal showed: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.