Discord is a native Crew channel — every DM thread is an isolated session with full tool access, streaming responses, and inline tool approval. Setup mirrors Slack, but the trust and allowlist model is stricter: deny-by-default with an explicit user allowlist.
The dashboard shows a green status badge when connected.
Set the env var and configure the allowlist:
# In ~/.kiro/crew/.env DISCORD_BOT_TOKEN=your-token-here
In ~/.kiro/crew/config.json:
{ "discord": { "allowed_users": ["123456789012345678"] } }
Then restart: kirocrew restart. Check status with kirocrew doctor.
Discord uses a Gateway WebSocket connection for inbound events (identify → heartbeat → message dispatch) and REST for outbound sends (post message, edit, react, ack component). No public URL is needed — Crew's process establishes and maintains the WebSocket.
Compared to Slack, the moving parts are:
| Aspect | Slack | Discord |
|---|---|---|
| Inbound | Socket Mode WebSocket | Gateway WebSocket |
| Outbound | Web API REST | REST |
| Auth model | Owner-only via KIROCREW_OWNER_ID | Deny-by-default allowlist of user IDs |
| Session key | slack:{thread_ts} | discord:{agent}:direct:{user} |
Each Discord DM channel becomes its own session, keyed by discord:{agent}:direct:{user}:
session.timeout_secs (default 30 min idle)Because the session is keyed on the user (not a thread), all messages from one user go into one continuous session. Discord DMs are conversational — the model fits.
Discord's control-plane surface is deny-by-default, even for the bot's owner. To interact with the bot from a Discord DM, the user must be in the allowlist.
Managed via config or the dashboard:
{ "discord": { "allowed_users": ["123456789012345678", "234567890123456789"] } }
Or through the dashboard's Discord settings panel. Every allowlist add is SEL-audited.
Why deny-by-default. Unlike Slack, where the owner ID gives a single hard-locked identity, Discord bot invites are cheap. A bot added to a public server with no allowlist would be exposed to anyone who could DM it. The allowlist forces a per-user opt-in.
The autonudge loop — the reactive self-check that reinjects instructions on an idle timer — is locked to the user's current session only on Discord. A nudge cannot fire against a user's older DM session, and a user not currently active gets nothing.
This asymmetry is intentional. Discord's channel model doesn't cleanly separate "this user's active work" from "this user's older threads", so Crew errs strict.
Interactive tool approval works via Discord's message component buttons:
Follow-up messages during an approval prompt are queued. Sending !stop cancels the current turn (and any pending approval).
Approval mode matches the gateway's global mode:
interactive — every tool call promptsauto (YOLO) — auto-approve all tool callsInteractive-mode caveat. In interactive mode, Discord babysit loops cannot use tools — the unattended turn has no one to approve calls, so it falls through to deny-by-default. If you want tool-heavy Discord workflows, run the gateway in auto mode or use the dashboard / Slack for tool work.
Discord's platform caps messages at 2000 characters per message (max_message_chars: 2000 for the Discord transport). Long agent responses are automatically split. Interactive buttons are capped at 3 per Block Kit-style card (max_buttons: 3).
file_send tool callsFiles sent by non-allowlisted users are silently dropped.
If your Discord bot runs in an organization with multiple accounts, the transport supports enterprise ID validation — analogous to Slack's Enterprise Grid check — but the primary defense is still the allowlist.
Discord group channels can be joined but must be enabled per-channel. In group channels:
By default, only DM sessions are active. Group participation is opt-in.
The send_message MCP tool supports Discord targets:
channel: "discord:{channel_id}" — post to a specific channeluser: "discord:{user_id}" — post to a specific user's DMCron jobs and subagent completions can target Discord this way if the routing marker declares a Discord channel:
- [ ] Poll status <!-- deliver:discord:{channel_id} -->
Discord's REST API has per-route rate limits. The transport respects them and backs off automatically. If you see repeated 429s in kirocrew doctor, the bot is being rate-limited — reduce message frequency or use fewer inline component updates.
| Symptom | Fix |
|---|---|
| Bot doesn't respond | Confirm DISCORD_BOT_TOKEN is set and kirocrew doctor reports Discord connected |
| Only owner works, no other user can DM | Add the user's Discord ID to discord.allowed_users via config or dashboard |
| Tool approval buttons don't do anything | Discord's interaction endpoint requires the bot to be online; check gateway logs for websocket disconnects |
| Long response gets cut off | 2000-char cap — the transport splits automatically; if you see truncation instead, check for a fragile custom message formatter |
| Voice memo not transcribed | Verify the STT provider is configured (voice.stt_provider); Discord DMs are treated like Slack voice memos |
Under discord.*:
| Key | Default | Purpose |
|---|---|---|
discord.enabled | false (until token present) | Master switch — implied on when token is set |
discord.allowed_users | [] | Deny-by-default allowlist of Discord user IDs |
discord.enterprise_id | "" | Optional enterprise / workspace ID check |
discord.command_prefix | ! | Prefix for bang commands |
Discord