arizuko’s Discord adapter (discd) connects one bot account to your Discord server and routes messages to per-group Claude agents. Each server channel gets its own agent, or you share one across the whole server — your routing table decides.
Discord messages travel through the stack in five steps:
InboundMsg with a typed JID.reply or send.Every Discord chat has a typed JID that routing rules match against:
| chat type | JID format | example |
|---|---|---|
| server channel | discord:<guild_id>/<channel_id> | discord:123456789/987654321 |
| server thread | discord:<guild_id>/<thread_id> | same format; thread ID is the channel |
| DM | discord:dm/<channel_id> | discord:dm/111222333 |
To get IDs: in Discord, open Settings → Advanced and enable Developer Mode. Then right-click any channel or server and choose Copy Channel ID / Copy Server ID.
Each inbound message carries a verb that describes what happened. Routes match on verb= to control which events fire the agent:
| verb | when |
|---|---|
| (empty) | regular message in a server channel or DM |
mention | message @mentions the bot, or is a reply to a bot message |
like | 👍 or similar positive reaction added |
dislike | 👎 or similar negative reaction added |
discd supports two authentication modes. Choose based on your use case:
| mode | env var | best for |
|---|---|---|
| Bot token | DISCORD_BOT_TOKEN | shared deployments, servers you don’t own, production |
| User token | DISCORD_USER_TOKEN | personal use, your own account, servers where adding a bot app isn’t possible |
DISCORD_BOT_TOKEN or DISCORD_USER_TOKEN. If both are set, the user token takes precedence. If neither is set, discd exits at startup.Skip this if you are using a user token.
.env file, not in code or chat.Skip this if you are using a user token (your account is already in your servers).
bot.like tool.delete tool (optional).Add to /srv/data/arizuko_<instance>/.env:
# bot mode (recommended)
DISCORD_BOT_TOKEN=your_bot_token_here
# user mode (personal use)
# DISCORD_USER_TOKEN=your_user_token_here
Then regenerate compose and restart:
arizuko run <instance>
sudo systemctl restart arizuko_<instance>
Watch logs to confirm the adapter connected:
sudo journalctl -u arizuko_<instance> --since "30s ago" --no-pager \
| grep -E "discord connected|channel registered"
You should see "discord connected" from discd and "channel registered" from gated.
Without a routing entry, messages from Discord are stored but no agent is fired. Add at least one route. The easiest ways:
Open /dash/ → Routing → Add route.
In the root group, ask the agent to add a route:
add a route: match all Discord messages, target folder "main"
The agent calls the add_route MCP tool. Changes take effect immediately (no restart needed).
mcpc connect "socat UNIX-CONNECT:/srv/data/arizuko_<inst>/data/ipc/main/gated.sock -" @s
mcpc @s tools-call add_route match:="platform=discord" target:="main"
mcpc @s close
Routes are matched in seq order (lowest first). The match field is a space-separated list of key=glob predicates. All predicates must match.
| match expression | what it routes |
|---|---|
platform=discord | all Discord messages (server channels + DMs) |
chat_jid=discord:<guild>/<channel> | one specific channel |
platform=discord verb=mention | only @mentions or replies to bot |
| (empty) | everything — catch-all, lowest priority |
match: platform=discord
target: main
Every message from any channel in any Discord server the bot is in routes to the main group.
match: chat_jid=discord:123456789/111111111
target: engineering
match: chat_jid=discord:123456789/222222222
target: marketing
match: platform=discord
target: main <-- catch-all for other channels
To make the agent respond only when @mentioned (ignoring regular chat), use a two-row pattern: one row that fires on mentions, one catch-all row in #observe mode that archives the rest into the same folder for context without firing a turn.
seq match target
10 platform=discord room=guild/* verb=mention main
20 platform=discord room=guild/* main#observe
Row 10 fires the agent on mentions and bot replies. Row 20 stores every other guild message under main so the agent has full thread context the next time it runs, but no turn is invoked. DMs are unaffected — add a separate route for them.
One thing to expect: after the agent replies in a channel thread, that (channel, thread) is engaged for 10 minutes. Subsequent messages in the same thread fire turns even without another @mention, even though row 20 says #observe. This is intentional — silencing a mid-conversation thread would be worse than the noise-reduction intent of the route. The window expires after 10 minutes of quiet.
See routing modes for the full fragment vocabulary.
Discord threads are treated as separate conversations. When a message arrives inside a thread, arizuko sets the topic field to the thread’s channel ID. The agent sees threads as distinct conversation threads — replies to a thread stay in that thread’s context.
Inside a container, the agent uses MCP tools:
# reply in the same channel/thread
reply text:="Here is your answer."
# send to a specific channel (by JID)
send chatJid:="discord:123456789/987654321" text:="Scheduled report ready."
# react to a message
like chatJid:="discord:123456789/987654321" targetId:="1234567890" emoji:="👍"
# send a file
send_file chatJid:="discord:123456789/987654321" \
filepath:="/home/node/tmp/report.pdf" filename:="report.pdf"
The agent can also edit and delete its own messages, and quote (reply with commentary).
send to a channel instead.discd exposes GET /health on its internal port. The adapter reports:
| status | HTTP | meaning |
|---|---|---|
| ok | 200 | connected and receiving messages |
| stale | 200 | connected but no inbound messages in 5 minutes (idle, not broken) |
| disconnected | 503 | WebSocket not open; check token validity and network |
sudo curl -s http://localhost:9002/health
(Port 9002 is the default; set DISCD_LISTEN to change it.)
/dash/ → Routing. Without a route, messages are stored but no agent is fired.DISCORD_BOT_TOKEN. For user mode: check that DISCORD_USER_TOKEN is still valid (user tokens invalidate on password change or forced logout). Restart after updating: sudo docker restart arizuko_discd_<instance>.send with a hardcoded JID. Use reply to respond to the originating chat automatically.verb=mention in its match, or the catch-all row is in #observe mode. Drop the verb constraint or remove the #observe fragment from the target to let all messages fire.| variable | required | default | notes |
|---|---|---|---|
DISCORD_BOT_TOKEN | one of | — | from Discord Developer Portal → Bot; required unless using user token |
DISCORD_USER_TOKEN | one of | — | personal user account token; takes precedence over bot token when both are set |
DISCD_LISTEN | no | :9002 | internal HTTP port for health and file serving |
DISCD_LISTEN_URL | no | http://discd:9002 | public URL for attachment proxying |
ASSISTANT_NAME | no | — | replaces <@bot_id> mentions with a readable name in stored messages |
MEDIA_MAX_FILE_BYTES | no | 20971520 (20 MB) | max attachment size to proxy |