Discord plugin: DM replies fail with 'channel not allowlisted' due to null recipientId

Resolved 💬 3 comments Opened Mar 31, 2026 by mtekgroup Closed Apr 4, 2026

Bug

The Discord plugin's outbound gate (fetchAllowedChannel) fails to reply to DM channels even when the user is correctly allowlisted in access.json.

Root cause

In server.ts, the inbound gate (gate()) correctly uses msg.author.id to check the allowlist (line 241/245):

const senderId = msg.author.id
// ...
if (access.allowFrom.includes(senderId)) return { action: 'deliver', access }

But the outbound gate (fetchAllowedChannel) relies on ch.recipientId from a fetched DMChannel (line 406-407):

if (ch.type === ChannelType.DM) {
    if (access.allowFrom.includes(ch.recipientId)) return ch
}

DMChannel.recipientId can be null when the channel is fetched via client.channels.fetch() with a cold cache (e.g., after plugin/session restart). This causes allowFrom.includes(null)false → throws "channel not allowlisted".

Reproduction

  1. Configure access.json with a user ID in allowFrom and dmPolicy: "allowlist"
  2. Restart the Claude Code session (cold Discord.js cache)
  3. Receive a DM from the allowlisted user (works — inbound uses msg.author.id)
  4. Try to reply via the reply tool → fails with "channel {id} is not allowlisted"

The bug is intermittent: it works when the DM channel is still in Discord.js's cache (e.g., within the same session after receiving a message), but breaks after restarts.

Suggested fix

Maintain a Map<channelId, userId> populated during inbound message handling, and use it as a fallback when recipientId is null:

// After recentSentIds
const dmChannelUsers = new Map<string, string>()

// In handleInbound, after gate() succeeds:
if (msg.channel.type === ChannelType.DM) {
    dmChannelUsers.set(msg.channelId, msg.author.id)
}

// In fetchAllowedChannel:
if (ch.type === ChannelType.DM) {
    const recipientId = ch.recipientId ?? dmChannelUsers.get(id)
    if (recipientId && access.allowFrom.includes(recipientId)) return ch
}

This is a minimal 3-line change that resolves the issue without changing the security model — the map is only populated for users who already passed the inbound gate.

Environment

  • Claude Code CLI
  • Discord plugin v0.0.4
  • macOS

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗