Skip to content

bug: pin/unpin/listPins Discord actions fail with SecretRef token accounts (missing cfgOptions pass-through) #50411

@PrinceOfEgypt

Description

@PrinceOfEgypt

Summary

pinMessage, unpinMessage, and listPins actions fail with the error:

channels.discord.accounts.<accountId>.token: unresolved SecretRef "env:default:DISCORD_TOKEN_<ACCOUNTID>". Resolve this command against an active gateway runtime snapshot before reading it.

This happens when using any non-default Discord account whose token is stored as a SecretRef (e.g. { source: "env", provider: "default", id: "DISCORD_TOKEN_ATHENAEUM" }) rather than a plaintext string.

Root Cause

In src/agents/tools/discord-actions-messaging.ts, inside handleDiscordMessagingAction, the function receives a cfg parameter and builds:

const cfgOptions = cfg ? { cfg } : {};

This cfgOptions object is correctly spread into most Discord action calls — for example, react:

// react — correct ✅
await reactMessageDiscord(channelId, messageId, emoji, { ...cfgOptions, accountId });

But pinMessage, unpinMessage, and listPins drop cfgOptions entirely:

// pinMessage — missing cfgOptions ❌
if (accountId) {
  await pinMessageDiscord(channelId, messageId, { accountId });
} else {
  await pinMessageDiscord(channelId, messageId);
}

Without cfg in the opts, resolveDiscordRest(opts) calls createDiscordRestClient(opts, opts.cfg) where opts.cfg is undefined. The second arg (cfg fallback) is also undefined because resolveDiscordRest passes opts.cfg directly rather than a live loadConfig() call. This causes createDiscordRestClient to fall back to loadConfig() which — in this code path — does not have the SecretRef already resolved into a real token string, so normalizeDiscordTokennormalizeResolvedSecretInputStringassertSecretInputResolved throws.

By contrast, when cfgOptions is spread correctly (as in react), the resolved cfg from the gateway runtime snapshot is passed through via opts.cfg, and createDiscordRestClient uses it directly, bypassing the unresolved-SecretRef path.

Affected actions

  • pinMessage
  • unpinMessage
  • listPins

Proposed Fix

In src/agents/tools/discord-actions-messaging.ts, spread cfgOptions into the opts objects for pin/unpin/listPins, consistent with how react and other actions handle it:

// pinMessage
case "pinMessage": {
  if (!isActionEnabled("pins")) throw new Error("Discord pins are disabled.");
  const channelId = resolveChannelId();
  const messageId = readStringParam(params, "messageId", { required: true });
  if (accountId) {
-   await pinMessageDiscord(channelId, messageId, { accountId });
+   await pinMessageDiscord(channelId, messageId, { ...cfgOptions, accountId });
  } else {
-   await pinMessageDiscord(channelId, messageId);
+   await pinMessageDiscord(channelId, messageId, cfgOptions);
  }
  return jsonResult({ ok: true });
}
case "unpinMessage": {
  if (!isActionEnabled("pins")) throw new Error("Discord pins are disabled.");
  const channelId = resolveChannelId();
  const messageId = readStringParam(params, "messageId", { required: true });
  if (accountId) {
-   await unpinMessageDiscord(channelId, messageId, { accountId });
+   await unpinMessageDiscord(channelId, messageId, { ...cfgOptions, accountId });
  } else {
-   await unpinMessageDiscord(channelId, messageId);
+   await unpinMessageDiscord(channelId, messageId, cfgOptions);
  }
  return jsonResult({ ok: true });
}
case "listPins": {
  if (!isActionEnabled("pins")) throw new Error("Discord pins are disabled.");
  const channelId = resolveChannelId();
  return jsonResult({
    ok: true,
-   pins: (accountId ? await listPinsDiscord(channelId, { accountId }) : await listPinsDiscord(channelId)).map((pin) => normalizeMessage(pin)),
+   pins: (accountId ? await listPinsDiscord(channelId, { ...cfgOptions, accountId }) : await listPinsDiscord(channelId, cfgOptions)).map((pin) => normalizeMessage(pin)),
  });
}

The same pattern likely applies in src/channels/plugins/actions/discord/handle-action.guild-admin.ts — any pin/unpin calls there should also receive cfgOptions if applicable.

Reproduction

  1. Configure a Discord account with a SecretRef token (e.g. channels.discord.accounts.mybot.token: { source: env, provider: default, id: DISCORD_TOKEN_MYBOT })
  2. From an agent session, call the message tool with action: react — succeeds ✅
  3. From the same session, call the message tool with action: pin, messageId, channel, and accountId — fails with the SecretRef error ❌

Environment

  • OpenClaw: latest (v2026.3.x)
  • Channel: Discord
  • Account token storage: env-sourced SecretRef
  • Discovered via: agent tool call investigation in the #athenaeum channel

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions