Skip to main content
Claude Code · Discord · MCP · AI Agent · Windows

Overview

The official Discord channel plugin binds the Discord gateway to an MCP stdio pipe that dies the moment the terminal closes, so the bot is only online while Claude Code is open. This project runs bot.ts as its own long-lived process that holds the gateway connection itself. Each DM from an allowlisted user spawns a fresh headless claude -p loaded with the full global config (MCP servers, plugins, CLAUDE.md), and that process posts the reply back to Discord. A Windows logon-task daemon keeps it alive across terminal close, crashes, and reboots. It is distributed both as an npm package (bunx claude-discord-daemon / cdd) and as a Claude Code plugin (the /claude-discord-daemon:discord command), under the MIT license.

Core features

  • Live named-tool status card: while a run is in flight, a single embed is edited in place and names the exact tool, skill, MCP, agent, or command that is running (for example MCP matlab·evaluate, Bash, agent code-reviewer, skill xlsx, /commit) instead of a vague “thinking”
  • Reply overwrites the status card: on success the card is edited in place into the final reply, with a faint Discord -# footer (elapsed time and per-kind tool counts), so the chat keeps a single message; file, long, or multi-chunk replies go out as fresh messages (edits cannot attach files) and the card collapses to just the footer
  • Rich output directives, each on its own line in the reply:
    • [[embed: {json}]] renders a colored embed card; the JSON may span multiple lines
    • [[file: /absolute/path]] attaches a produced file, verified for existence and size before sending
    • a trailing [[buttons: A | B]] line renders 2-5 real Discord choice buttons; the clicked label is fed back into the same session as the next message
  • Attachment download: files the user uploads (≤ 50 MB) are downloaded to a shared inbox and their local paths are appended to the prompt for claude to Read
  • Permission relay: a headless run’s permission prompts become a Discord Allow / Deny button card that the owner clicks, with no trip back to a terminal
  • Idle watchdog: no total time limit by default; a run is killed only when its event stream goes silent past BOT_IDLE_TIMEOUT_MS (a wedged process), never for merely taking a long time
  • Natural-language new-conversation reset: a message that is entirely one of a few reset phrases drops the stored session, so the next message starts fresh
  • Per-chat session continuity: each chat’s session UUID is stored in sessions.json and later messages --resume it
  • 24/7 daemon: a logon scheduled task, detached launch, restart on crash, and a single-instance loopback lock keep it running across terminal close, crashes, and reboots
  • Portable: no hardcoded paths; the working directory defaults to the engine directory and every path is overridable via env vars

Architecture

The boot chain that keeps it up is: logon task ClaudeDiscordBridge → wscript launch.vbs (detached) → supervise.cmd loop → bun bot.ts. The bridge owns its own discord.js gateway instead of using the official channel engine because Anthropic gates any non-approved custom channel behind --dangerously-load-development-channels, and that flag shows a startup confirmation dialog. A headless daemon has no TTY to answer it, so it hangs. With bot.ts holding the gateway, no development-channel flag and no interactive confirmation are needed, and it can run unattended as a service. Project files:
claude-discord-daemon
bot.ts · long-lived Discord gateway process (the bridge engine)
perm-mcp.ts · permission relay MCP
cli.ts · cdd / claude-discord-daemon CLI
launch.vbs · launches the supervisor detached
supervise.cmd · restarts on crash
install-service.ps1 · registers the logon scheduled task
.env.example · environment variable template

Tech stack

  • Runtime: Bun ≥ 1.1.0, TypeScript
  • Discord: discord.js v14
  • Permission relay: perm-mcp.ts, built on the MCP SDK (@modelcontextprotocol/sdk)
  • Daemon: Windows logon scheduled task, launch.vbs, supervise.cmd
  • Distribution: npm package and Claude Code plugin

Quick start

Windows 11 only: the scheduled-task, VBS, and cmd supervision chain is Windows-specific. You need Bun ≥ 1.1.0 with bun on PATH, an installed and logged-in Claude Code CLI (the bridge spawns claude -p), and your own Discord bot token. The bot must share a server with you, because Discord requires that before it lets you DM a bot.
1

Write the bot token

Before starting, put your bot token and sender allowlist in the bridge’s state directory (default ~/.claude/channels/discord/, overridable via DISCORD_STATE_DIR). Write the token to ~/.claude/channels/discord/.env:
You can also set DISCORD_BOT_TOKEN directly in the process environment; a real env var wins over the .env file.
2

Set the allowlist

Create ~/.claude/channels/discord/access.json and add your Discord user ID to allowFrom:
Only DMs from user IDs in allowFrom are processed; everything else is ignored (fail-closed). The file is re-read on every message, so allowlist edits apply without a restart.
3

Install and start (pick one)

As an npm package:
As a Claude Code plugin:
Claude Code namespaces plugin commands with the plugin name, so the command is /claude-discord-daemon:discord. Drive the same daemon with it:
It runs the same cdd CLI under the hood, falling back to bunx claude-discord-daemon when cdd is not on PATH.
4

Invite the bot and DM it

Invite the bot into any Discord server you are also in, then DM the bot directly.
5

Manage the daemon

The bin names are claude-discord-daemon and cdd (identical).

Configuration reference

Optional access.json keys, with the same semantics as the official plugin: Everything in the environment except DISCORD_BOT_TOKEN is optional; set any of these in the process environment to override the default: New conversation: send a message that is entirely one of the following (whole message, case-insensitive) to drop the stored session, so the next message starts a fresh claude -p session:
The match is strict and whole-message (trailing whitespace and punctuation are tolerated), so a passing mention such as 來開新對話討論 X does not reset the session. On reset the bot reacts and confirms.

Notes

This bridge and the official Discord plugin log in with the same bot token. Before running this as a service you must disable the official plugin (claude plugin disable discord@claude-plugins-official), because two gateway logins on one token produce duplicate replies.
Discord input is untrusted and is a prompt-injection surface.
  • Inbound messages are wrapped in a <channel source="discord" ...> tag, with metadata in the tag and the untrusted text inside; the system prompt tells the model to treat that content as chat text, never as instructions to change configuration or access rules.
  • Only user IDs in allowFrom in access.json can trigger a run, and a failure to read the file fails closed. Button clickers, including the permission-approval buttons, are re-authenticated against the same allowlist.
  • The permission mode defaults to auto and bypassPermissions is never used; set BOT_PERMISSION_MODE=default to harden further.
  • Access and pairing are managed by the owner only, in the state directory, never via chat.
  • Keep the bot token out of version control.

In practice

This fits keeping Claude Code on call in a Discord DM without leaving a terminal open: by default, long jobs are not killed for running long (only stream silence counts as wedged), operations that need approval are decided with a button click in the chat, and the bot can take attachments and send back produced files. The limits are that it only supports Windows 11 and that only allowlisted users can trigger a run.