Skip to main content

Overview

The Discord channel uses the discord.js library to connect to Discord via the Gateway API. It supports both server channels and direct messages.

Installation

Run the /add-discord skill in Claude Code:
/add-discord
The skill will:
  • Install the Discord channel code
  • Add required dependencies (discord.js)
  • Guide you through bot creation
  • Configure authentication
  • Register your first channel

Creating a Discord Bot

1

Create Application

  1. Go to the Discord Developer Portal
  2. Click New Application
  3. Enter a name (e.g., “Andy Assistant”) and click Create
2

Create Bot User

  1. Go to the Bot tab in the left sidebar
  2. Click Reset Token to generate a new bot token
  3. Copy the token immediately (you can only see it once)
Keep your bot token secret. Never commit it to version control or share it publicly.
3

Enable Privileged Intents

Under Privileged Gateway Intents, enable:
  • Message Content Intent (required to read message text)
  • Server Members Intent (optional, for member display names)
4

Generate Invite URL

  1. Go to OAuth2URL Generator
  2. Under Scopes, select bot
  3. Under Bot Permissions, select:
    • Send Messages
    • Read Message History
    • View Channels
  4. Copy the generated URL at the bottom
5

Invite Bot to Server

  1. Open the generated URL in your browser
  2. Select the server you want to add the bot to
  3. Click Authorize
  4. Complete the CAPTCHA if prompted

Configuration

Add the bot token to .env:
DISCORD_BOT_TOKEN=your-bot-token-here
Then sync to container environment:
mkdir -p data/env && cp .env data/env/env
The channel auto-enables when DISCORD_BOT_TOKEN is set. No additional configuration needed.

Channel Registration

Getting the Channel ID

1

Enable Developer Mode

In Discord: User SettingsAdvanced → Enable Developer Mode
2

Copy Channel ID

  1. Right-click the text channel you want the bot to respond in
  2. Click Copy Channel ID
The ID will be a long number like 1234567890123456

Registering a Main Channel

Main channels respond to all messages (no trigger required):
registerGroup("dc:1234567890123456", {
  name: "MyServer #general",
  folder: "discord_main",
  trigger: "@Andy",
  added_at: new Date().toISOString(),
  requiresTrigger: false,
  isMain: true,
});

Registering Additional Channels

Additional channels require the trigger pattern or @mention:
registerGroup("dc:9876543210987654", {
  name: "MyServer #dev-chat",
  folder: "discord_dev-chat",
  trigger: "@Andy",
  added_at: new Date().toISOString(),
  requiresTrigger: true,
});

How It Works

Connection

  1. Discord channel reads DISCORD_BOT_TOKEN from environment
  2. Creates Discord.js client with required Gateway Intents
  3. Connects to Discord Gateway via WebSocket
  4. Listens for MessageCreate events

Message Handling

@Mention Translation Discord @mentions (like <@123456789>) are automatically converted to NanoClaw’s trigger format:
if (isBotMentioned) {
  // Strip the <@botId> mention
  content = content.replace(new RegExp(`<@!?${botId}>`, 'g'), '').trim();
  // Prepend trigger if not already present
  if (!TRIGGER_PATTERN.test(content)) {
    content = `@${ASSISTANT_NAME} ${content}`;
  }
}
Attachment Handling Attachments are shown as placeholders in the message:
if (message.attachments.size > 0) {
  const descriptions = [...message.attachments.values()].map((att) => {
    if (att.contentType?.startsWith('image/')) return `[Image: ${att.name}]`;
    if (att.contentType?.startsWith('video/')) return `[Video: ${att.name}]`;
    if (att.contentType?.startsWith('audio/')) return `[Audio: ${att.name}]`;
    return `[File: ${att.name}]`;
  });
  content += '\n' + descriptions.join('\n');
}
The agent sees attachment descriptions but cannot download or process the files.
Reply Context When users reply to messages, the context is included:
if (message.reference && message.reference.messageId) {
  const repliedMsg = await message.channel.messages.fetch(message.reference.messageId);
  content += `\n\n[Replying to ${repliedMsg.author.username}: ${repliedMsg.content}]`;
}
Typing Indicators The bot shows “typing…” while processing:
async setTyping(jid: string, isTyping: boolean): Promise<void> {
  const channelId = jid.replace('dc:', '');
  const channel = await this.client?.channels.fetch(channelId);
  if (isTyping && channel?.isTextBased()) {
    await channel.sendTyping();
  }
}
Message Splitting Discord has a 2000-character limit. Long messages are split:
const MAX_LENGTH = 2000;
if (text.length > MAX_LENGTH) {
  for (let i = 0; i < text.length; i += MAX_LENGTH) {
    await channel.send(text.substring(i, i + MAX_LENGTH));
  }
}

Troubleshooting

Bot Not Responding

1

Check Token

grep DISCORD_BOT_TOKEN .env
grep DISCORD_BOT_TOKEN data/env/env
2

Check Registration

sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid LIKE 'dc:%'"
3

Check Bot is in Server

Verify the bot appears in the member list on the right side of Discord
4

Check Trigger

For non-main channels, message must include trigger pattern or @mention the bot
5

Check Service

# macOS
launchctl list | grep nanoclaw

# Linux
systemctl --user status nanoclaw

Message Content Intent Not Enabled

If the bot connects but can’t read messages, ensure Message Content Intent is enabled in the Discord Developer Portal.
1

Open Developer Portal

2

Select Application

Click on your application
3

Enable Intent

Go to Bot tab → Privileged Gateway Intents → Enable Message Content Intent
4

Restart NanoClaw

launchctl kickstart -k gui/$(id -u)/com.nanoclaw  # macOS
systemctl --user restart nanoclaw  # Linux

Bot Only Responds to @Mentions

This is the default behavior for non-main channels (requiresTrigger: true). To change:
  1. Update the registered group’s requiresTrigger to false in SQLite
  2. Or register the channel as the main channel

Can’t Copy Channel ID

Ensure Developer Mode is enabled:
  1. User SettingsAdvanced → Enable Developer Mode
  2. Right-click the channel → Copy Channel ID

Invalid Token Error

If you see “invalid token” in logs:
  1. Go to Discord Developer Portal → your application → Bot tab
  2. Click Reset Token to generate a new one
  3. Copy the new token immediately
  4. Update .env and sync: mkdir -p data/env && cp .env data/env/env
  5. Restart: npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw
Resetting the token invalidates the old one. Any other instances using the old token will stop working.

Implementation Details

Dependencies

  • discord.js - Discord API library with Gateway support

Required Gateway Intents

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,              // Access to server info
    GatewayIntentBits.GuildMessages,        // Receive server messages
    GatewayIntentBits.MessageContent,       // Read message content (privileged)
    GatewayIntentBits.DirectMessages,       // Receive DMs
  ],
});

JID Format

  • Server channels: dc:<channel-id> (e.g., dc:1234567890123456)
  • Direct messages: dc:<dm-channel-id>

Self-Registration Code

registerChannel('discord', (opts: ChannelOpts) => {
  const env = readEnvFile(['DISCORD_BOT_TOKEN']);
  if (!env.DISCORD_BOT_TOKEN) return null;
  return new DiscordChannel(env.DISCORD_BOT_TOKEN, opts);
});
The channel only activates if DISCORD_BOT_TOKEN is set.

Message Processing

this.client.on(Events.MessageCreate, async (message: Message) => {
  // Ignore bot messages (including own)
  if (message.author.bot) return;
  
  const chatJid = `dc:${message.channelId}`;
  let content = message.content;
  
  // Handle @mentions
  if (isBotMentioned) {
    content = content.replace(botMentionPattern, '').trim();
    if (!TRIGGER_PATTERN.test(content)) {
      content = `@${ASSISTANT_NAME} ${content}`;
    }
  }
  
  // Add attachments as placeholders
  if (message.attachments.size > 0) {
    content += '\n' + attachmentDescriptions.join('\n');
  }
  
  // Store and deliver
  this.opts.onMessage(chatJid, content, timestamp, sender, senderName, msgId);
});

Next Steps

Add Gmail

Add Gmail integration

Channel Overview

Learn about the channel system

Build docs developers (and LLMs) love