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 normalizeDiscordToken → normalizeResolvedSecretInputString → assertSecretInputResolved 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
- Configure a Discord account with a SecretRef token (e.g.
channels.discord.accounts.mybot.token: { source: env, provider: default, id: DISCORD_TOKEN_MYBOT })
- From an agent session, call the
message tool with action: react — succeeds ✅
- 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
Summary
pinMessage,unpinMessage, andlistPinsactions fail with the error: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, insidehandleDiscordMessagingAction, the function receives acfgparameter and builds:This
cfgOptionsobject is correctly spread into most Discord action calls — for example,react:But
pinMessage,unpinMessage, andlistPinsdropcfgOptionsentirely:Without
cfgin the opts,resolveDiscordRest(opts)callscreateDiscordRestClient(opts, opts.cfg)whereopts.cfgisundefined. The second arg (cfgfallback) is alsoundefinedbecauseresolveDiscordRestpassesopts.cfgdirectly rather than a liveloadConfig()call. This causescreateDiscordRestClientto fall back toloadConfig()which — in this code path — does not have the SecretRef already resolved into a real token string, sonormalizeDiscordToken→normalizeResolvedSecretInputString→assertSecretInputResolvedthrows.By contrast, when
cfgOptionsis spread correctly (as inreact), the resolvedcfgfrom the gateway runtime snapshot is passed through viaopts.cfg, andcreateDiscordRestClientuses it directly, bypassing the unresolved-SecretRef path.Affected actions
pinMessageunpinMessagelistPinsProposed Fix
In
src/agents/tools/discord-actions-messaging.ts, spreadcfgOptionsinto the opts objects for pin/unpin/listPins, consistent with howreactand other actions handle it:The same pattern likely applies in
src/channels/plugins/actions/discord/handle-action.guild-admin.ts— any pin/unpin calls there should also receivecfgOptionsif applicable.Reproduction
channels.discord.accounts.mybot.token: { source: env, provider: default, id: DISCORD_TOKEN_MYBOT })messagetool withaction: react— succeeds ✅messagetool withaction: pin,messageId,channel, andaccountId— fails with the SecretRef error ❌Environment
env-sourced SecretRef