arizuko

arizukohowto › Discord

deploy on Discord

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.

how it works

Discord messages travel through the stack in five steps:

  1. discd receives the message via Discord’s Gateway WebSocket and converts it to an internal InboundMsg with a typed JID.
  2. routd stores the message and looks it up in the routing table. The first matching route determines which group folder receives it.
  3. routd dispatches the turn; runed spawns a Docker container with the agent for that group and delivers the message.
  4. The agent runs, calls MCP tools, and eventually calls reply or send.
  5. discd sends the agent’s response back to the Discord channel.

JID format

Every Discord chat has a typed JID that routing rules match against:

chat typeJID formatexample
server channeldiscord:<guild_id>/<channel_id>discord:123456789/987654321
server threaddiscord:<guild_id>/<thread_id>same format; thread ID is the channel
DMdiscord: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.

verb system

Each inbound message carries a verb that describes what happened. Routes match on verb= to control which events fire the agent:

verbwhen
(empty)regular message in a server channel or DM
mentionmessage @mentions the bot, or is a reply to a bot message
like👍 or similar positive reaction added
dislike👎 or similar negative reaction added

token modes

discd supports two authentication modes. Choose based on your use case:

modeenv varbest for
Bot tokenDISCORD_BOT_TOKENshared deployments, servers you don’t own, production
User tokenDISCORD_USER_TOKENpersonal use, your own account, servers where adding a bot app isn’t possible
One token, not both. Set exactly one of DISCORD_BOT_TOKEN or DISCORD_USER_TOKEN. If both are set, the user token takes precedence. If neither is set, discd exits at startup.
User token risks: Using a user token runs the agent as your personal Discord account. Discord’s ToS restricts automated use of user accounts. Use on accounts you own and can afford to lose. Bot tokens are the safe choice for any production or team deployment.

step 1 — create a Discord application (bot mode)

Skip this if you are using a user token.

  1. Go to discord.com/developers/applications and click New Application.
  2. Name it (this is the app name, not the bot username).
  3. Go to Bot in the left sidebar. Click Add Bot if prompted.
  4. Under Privileged Gateway Intents, enable:
    Message Content Intent — required; without it discd receives messages with empty content.
    Server Members Intent — needed if you want to resolve member names in DMs.
    Presence Intent — optional.
  5. Click Reset Token and copy the bot token. Store it; you won’t see it again.
Token security: treat tokens like passwords. Store them only in your instance’s .env file, not in code or chat.

step 2 — invite the bot to your server (bot mode)

Skip this if you are using a user token (your account is already in your servers).

  1. In the application, go to OAuth2 → URL Generator.
  2. Select scope: bot.
  3. Select permissions:
    View Channels, Read Message History, Send Messages — required.
    Attach Files — for file and voice responses.
    Add Reactions — for like tool.
    Manage Messages — for delete tool (optional).
  4. Copy the generated URL and open it in a browser. Select your server and click Authorize.

step 3 — configure arizuko

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 routd.

step 4 — set up routing

Without a routing entry, messages from Discord are stored but no agent is fired. Add at least one route. The easiest ways:

via the operator dashboard

Open /dash/RoutingAdd route.

via the root agent (in chat)

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).

via mcpc (shell)

mcpc connect "socat UNIX-CONNECT:/srv/data/arizuko_<inst>/ipc/main/gated.sock -" @s
mcpc @s tools-call add_route match:="platform=discord" target:="main"
mcpc @s close

routing patterns

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 expressionwhat it routes
platform=discordall Discord messages (server channels + DMs)
chat_jid=discord:<guild>/<channel>one specific channel
platform=discord verb=mentiononly @mentions or replies to bot
(empty)everything — catch-all, lowest priority

common setup: one agent per server

match: platform=discord
target: main

Every message from any channel in any Discord server the bot is in routes to the main group.

common setup: one agent per channel

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

mention-only 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.

threads

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.

sending to Discord from the agent

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).

post is not available on Discord — Discord has no broadcast-feed primitive. Use send to a channel instead.

health check

discd exposes GET /health on its internal port. The adapter reports:

statusHTTPmeaning
ok200connected and receiving messages
stale200connected but no inbound messages in 5 minutes (idle, not broken)
disconnected503WebSocket 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.)

troubleshooting

Bot is in the server but doesn’t respond
Check that a route exists: /dash/ → Routing. Without a route, messages are stored but no agent is fired.
Messages arrive with empty content (bot mode)
Message Content Intent is not enabled. Go to Discord Developer Portal → Bot → Privileged Gateway Intents and toggle it on. User mode does not require intents.
discd shows “disconnected” in health
The token is wrong or has been reset. For bot mode: regenerate in the Developer Portal and update 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>.
Agent responds but replies go to the wrong channel
The agent is using send with a hardcoded JID. Use reply to respond to the originating chat automatically.
Only @mentions fire the agent, not regular messages
The matching route has 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.
Bot doesn’t see messages in a channel
Check that the bot has View Channel permission for that specific channel in Discord.

env vars reference

variablerequireddefaultnotes
DISCORD_BOT_TOKENone offrom Discord Developer Portal → Bot; required unless using user token
DISCORD_USER_TOKENone ofpersonal user account token; takes precedence over bot token when both are set
DISCD_LISTENno:9002internal HTTP port for health and file serving
DISCD_LISTEN_URLnohttp://discd:9002public URL for attachment proxying
ASSISTANT_NAMEnoreplaces <@bot_id> mentions with a readable name in stored messages
MEDIA_MAX_FILE_BYTESno20971520 (20 MB)max attachment size to proxy