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:
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
Create Bot User
- Go to the Bot tab in the left sidebar
- Click Reset Token to generate a new bot token
- 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.
Enable Privileged Intents
Under Privileged Gateway Intents, enable:
- Message Content Intent (required to read message text)
- Server Members Intent (optional, for member display names)
Generate Invite URL
- Go to OAuth2 → URL Generator
- Under Scopes, select
bot
- Under Bot Permissions, select:
Send Messages
Read Message History
View Channels
- Copy the generated URL at the bottom
Invite Bot to Server
- Open the generated URL in your browser
- Select the server you want to add the bot to
- Click Authorize
- 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
Enable Developer Mode
In Discord: User Settings → Advanced → Enable Developer Mode
Copy Channel ID
- Right-click the text channel you want the bot to respond in
- 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
- Discord channel reads
DISCORD_BOT_TOKEN from environment
- Creates Discord.js client with required Gateway Intents
- Connects to Discord Gateway via WebSocket
- 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
Check Token
grep DISCORD_BOT_TOKEN .env
grep DISCORD_BOT_TOKEN data/env/env
Check Registration
sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid LIKE 'dc:%'"
Check Bot is in Server
Verify the bot appears in the member list on the right side of Discord
Check Trigger
For non-main channels, message must include trigger pattern or @mention the bot
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.
Select Application
Click on your application
Enable Intent
Go to Bot tab → Privileged Gateway Intents → Enable Message Content Intent
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:
- Update the registered group’s
requiresTrigger to false in SQLite
- Or register the channel as the main channel
Can’t Copy Channel ID
Ensure Developer Mode is enabled:
- User Settings → Advanced → Enable Developer Mode
- Right-click the channel → Copy Channel ID
Invalid Token Error
If you see “invalid token” in logs:
- Go to Discord Developer Portal → your application → Bot tab
- Click Reset Token to generate a new one
- Copy the new token immediately
- Update
.env and sync: mkdir -p data/env && cp .env data/env/env
- 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
],
});
- 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