claude-code-discord-multibot

mcp
Security Audit
Fail
Health Warn
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 5 GitHub stars
Code Fail
  • child_process — Shell command execution capability in patch-discord-plugin.sh
  • process.env — Environment variable access in patch-discord-plugin.sh
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Using Discord bots easily deploy multiple Claude code tasks (A Sibling repo with claude-code-telegram-multibot) 使用Discord快速部署多个Claude code终端(claude-code-telegram-multibot的姐妹仓库)

README.md

🇺🇸English · 🇨🇳中文

claude-code-discord-multibot

One dedicated Discord bot per Claude Code project · independent state · zero cross-talk · scales to hundreds of bots

Sibling project: claude-code-telegram-multibot — same architecture on Telegram. Pick this Discord version if you've outgrown Telegram's 20-bot @BotFather cap, or want Discord-only features like fetch_messages and bidirectional reactions.

TL;DR — pair the official discord plugin with a few shell functions and a Claude Code skill, so each project directory gets its own Discord bot. Switch projects in Discord by switching channels, not terminals. Sister project to claude-code-telegram-multibot — same architecture, different platform.

Use Discord when you've outgrown Telegram's 20-bot hard cap, or want richer features: message history via fetch_messages, bidirectional reactions, threads, channel-based organization.

✨ Why

The official Discord plugin assumes one bot per user. If you want multiple Claude Code projects each bridged to a different Discord channel, the default setup runs into two walls:

  • Every claude invocation spawns the plugin's MCP server, which uses a singleton state directory by default. Each new session disrupts the previous one.
  • The /discord:access skill hardcodes the default state directory path, so per-project pairing workflows fail.

The trick: the plugin's server.ts honors a DISCORD_STATE_DIR environment variable. Point it at a unique directory per project and you get full isolation — separate token, separate pairing, separate bot server, no fighting.

The shell functions in claude-dc.bash derive DISCORD_STATE_DIR from basename "$PWD", so launching claude-dc in any project directory automatically targets that project's own bot. The companion skill (SKILL.md) teaches Claude Code agents the architecture, so saying "add a new Discord bot for this project" becomes a one-shot operation.

This skill / architecture is also not limited to Claude Code — it can be ported to Codex, OpenClaw, or other agent platforms. And the same idea works for any chat platform: Telegram (sibling repo), Slack, IRC, Matrix, iMessage, etc. — wherever you have a per-token-per-channel bot model.

🚀 Quick start

Prereqs: Claude Code installed · official discord plugin enabled in ~/.claude/settings.json · bun on $PATH · org policy permits the discord channel plugin (admin must add it to allowedChannelPlugins) · Discord account + a Discord server you control.

# 1. Install
git clone https://github.com/Lihan-Zhong/claude-code-discord-multibot.git
cd claude-code-discord-multibot

# 2. Load the shell functions
echo "source $PWD/claude-dc.bash" >> ~/.bashrc
source ~/.bashrc

# 3. Install the skill (teaches Claude Code to manage the setup)
mkdir -p ~/.claude/skills/setup-discord-multibot
cp SKILL.md ~/.claude/skills/setup-discord-multibot/SKILL.md

# 4. Optional but recommended: the plugin patches and the two hooks (see "Mechanisms" below)
cp patch-discord-plugin.sh ~/.claude/ && chmod +x ~/.claude/patch-discord-plugin.sh
mkdir -p ~/.claude/hooks && cp hooks/*.py ~/.claude/hooks/
~/.claude/patch-discord-plugin.sh          # idempotent; safe to re-run any time

⚠️ The hooks must be registered in ~/.claude/settings.json, never settings.local.json
see Mechanisms for the exact block and why.

Add a bot for a project:

cd /path/to/my-project           # whatever project

# At https://discord.com/developers/applications:
#   1. New Application → name it
#   2. Bot tab → Reset Token → copy
#   3. Enable Message Content Intent
#   4. OAuth2 → URL Generator → scope: bot + permissions → invite to your server

claude-dc-init                   # paste the token; writes a .env
claude-dc                        # launch Claude Code with this bot attached

# In Discord: DM your new bot any plain message (e.g. "hi")
# Bot replies with a 6-char pair code
claude-dc-pair <6-char-code>     # if jq is installed
# Or in the Claude Code session: "I got the pair code, please pair me."

Done. Future cd /path/to/my-project && claude-dc re-attaches to the same bot.

🚀 Much easier quick start

# 1. Install
git clone https://github.com/Lihan-Zhong/claude-code-discord-multibot.git
cd claude-code-discord-multibot

Then, ask your Claude Code session to read this whole repo and follow SKILL.md. The agent will walk you through every step 🔥

‼️ Recommended usage

  • Step 1: deploy your first Discord bot for your $HOME directory — connect your "manager" Claude Code terminal here.
  • Step 2: use that "manager" bot to help you deploy subsequent project-specific bots (the manager handles state dir / .env / pairing for you).
  • Then: you have an army of project-specific Discord bots, each in its own channel or DM, easy to switch between.

📁 What's in this repo

  • claude-dc.bash — six shell functions: claude-dc, claude-dc-init, claude-dc-alt, claude-dc-pair, and the two session-attributing resume helpers claude-dc-resume / claude-dc-alt-resume. Source from ~/.bashrc.
  • patch-discord-plugin.sh — three idempotent patches to the plugin's server.ts: bot-to-bot messages, a real online presence, and markdown-safe message splitting. Re-runnable; self-heals after a plugin upgrade.
  • hooks/enforce-discord-reply.pyStop hook. A turn triggered from Discord cannot end without a Discord reply.
  • hooks/guard-variant-memory.pyPreToolUse hook. An alt bot cannot write into another bot's memory namespace.
  • SKILL.md — Claude Code skill that teaches an agent the architecture. Drop into ~/.claude/skills/setup-discord-multibot/SKILL.md.
  • README.md / README.zh.md — this file (English / 中文).
  • CHANGELOG.md — what changed between releases.
  • LICENSE — MIT.
  • .gitignore — keeps tokens and state dirs out of git by default.

🧩 Two bots in the same project (alt mode)

Want two independent Claude Code sessions on the same project — a "primary" run and a "what if I tried it this way" experiment? Use claude-dc-alt:

cd /path/to/my-project

# After setting up the primary bot:
mkdir -p ~/.claude-discord/$(basename "$PWD")-2
$EDITOR ~/.claude-discord/$(basename "$PWD")-2/.env   # paste a second bot's token

claude-dc-alt        # variant 2 by default
claude-dc-alt 3      # variant 3, if you want a third

claude-dc-alt also exports CLAUDE_BOT_VARIANT=N, so your project rules can pick a variant-specific sandbox directory:

# in your CLAUDE.md or project rules
SANDBOX="Intermediate_data/for_claude${CLAUDE_BOT_VARIANT:+_${CLAUDE_BOT_VARIANT}}"

Bot A writes to Intermediate_data/for_claude/, bot A-alt writes to Intermediate_data/for_claude_2/. No collisions.

Resuming the right session

Claude Code keys sessions on the working directory alone — nothing binds a session to DISCORD_STATE_DIR. So in a two-bot directory, -c continues whichever transcript is newest, which is a coin flip, and -r's picker lists both siblings without saying which is which. Two helpers do the attribution for you:

claude-dc-resume         # resume the PRIMARY bot's own session
claude-dc-alt-resume 2   # resume alt 2's own session
claude-dc-resume <sid>   # escape hatch: resume exactly this session id

They use different signals, and that difference is the interesting part. The alt is easy: score each transcript by how often it references memory/variant_<N>/, its own namespace. The primary cannot just invert that test — its transcript mentions variant_2 plenty, because you discuss the alt bot inside it. What separates them is where the mention appears: score only the session opening, which is what the harness injected, not what the conversation later discussed. On a real five-session directory whole-file scoring could not separate the two bots at all, while opening-only scoring gave 0 for the primary and 3–4 for each alt.

Neither helper guesses: if nothing scores cleanly, it starts fresh and tells you.

🛡️ Mechanisms (hooks + plugin patches)

v1 shipped conventions. The problem with a convention is that it fails exactly when you need it — at the end of a long tool chain, under load. v2 moves the important ones down into the harness.

what it does shape
hooks/enforce-discord-reply.py Stop hook — blocks ending a Discord-triggered turn that never called reply/react/edit_message guard (denies)
hooks/guard-variant-memory.py PreToolUse hook — denies a write into any memory namespace that is not this alt's own memory/variant_N/ guard (denies)
patch-discord-plugin.sh #1 lets bots see each other's messages in a shared channel (drops only self-echo) transform
patch-discord-plugin.sh #2 a real online presence, so the green dot means "actually connected" transform
patch-discord-plugin.sh #3 markdown-safe 2000-char splitting — a code block is never cut in half transform (repairs)

Prefer a transform to a guard where one exists: a guard still produces an event someone has to notice and act on, while a transform has no failure mode to report — the right thing simply comes out. Nobody has to know the chunker exists.

Both hooks are fail-open: bad input, unknown tool, missing transcript, or any internal error → allow. A guard that wedges every session is worse than the slip it prevents.

⚠️ Hooks only load from ~/.claude/settings.json

This one cost two weeks of a hook that never ran, with no error and no warning. Claude Code's settings chain is:

<project>/.claude/settings.local.json → <project>/.claude/settings.json → ~/.claude/settings.json

~/.claude/settings.local.json is not on that chain. A hooks block placed there is silently ignored. Register them like this:

// ~/.claude/settings.json
{
  "hooks": {
    "Stop": [
      { "hooks": [{ "type": "command", "command": "/usr/bin/python3 $HOME/.claude/hooks/enforce-discord-reply.py" }] }
    ],
    "PreToolUse": [
      { "matcher": "Write|Edit|NotebookEdit|Bash",
        "hooks": [{ "type": "command", "command": "/usr/bin/python3 $HOME/.claude/hooks/guard-variant-memory.py" }] }
    ]
  }
}

And do not verify this from a bot whose project directory is your $HOME. That is the one session where ~/.claude/settings.local.json is the project-level file, so it happens to work there and nowhere else — which is exactly how the bug hid for two weeks. Test from a real project bot.

Keeping the patches alive

patch-discord-plugin.sh edits a file the plugin owns, so a plugin upgrade overwrites it. That is handled, but it is worth knowing how:

  • The launcher calls the script on every shell start; it is idempotent, so it is a no-op until it is needed.
  • The running plugin loads from the cache copy (~/.claude/plugins/cache/.../discord/<version>/server.ts), pinned by version. An upgrade creates a new version directory, and the patch target is a glob, so it is covered.
  • If upstream renames chunk() entirely, the script prints [warn] and leaves the file alone rather than writing something broken. Patch #3 additionally runs a transpile check and reverts itself if the result no longer parses.
  • Belt and braces: set "chunkMode": "newline" in each bot's access.json. It does nothing while patch #3 is in place, and it means that with the patch entirely absent you still get clean line breaks instead of a hard slice(0, 2000).

A running bot keeps the old code until it restarts. Patching does not affect a live session.

🤖 Multi-bot collaboration (Discord-specific)

Unlike Telegram, Discord lets multiple bots share a channel and see each other's messages. This enables a "team of agents in one room" workflow — e.g. @planner_bot, @coder_bot, @reviewer_bot all in one channel coordinating via @mentions.

Trade-off: bots in shared channels see all channel messages as <channel> events even when they don't act, which inflates each bot's session context. Recommended pattern: keep bots in isolated channels for daily work; create temporary "war-room" channels when you need real cross-bot collaboration, then archive when done.

See SKILL.md "Multi-bot collaboration via shared channels" for details.

🗂️ State directory layout

~/.claude-discord/
├── <project-A-basename>/
│   ├── .env                   # DISCORD_BOT_TOKEN=...   (chmod 600)
│   ├── access.json            # dmPolicy / allowFrom / pending (chmod 600)
│   ├── approved/<senderId>    # pairing-confirm signal file (contents: chatId)
│   └── inbox/                 # received attachments (photos etc.)
├── <project-A-basename>-2/    # alt bot for project A
└── <project-B-basename>/
    └── ...

🐛 Known issues

See SKILL.md for the full troubleshooting catalogue.

  • A hook you registered never runs. It is almost certainly in ~/.claude/settings.local.json, which is not on the settings chain. Move the hooks block to ~/.claude/settings.json. There is no error message for this. See Mechanisms.
  • The plugin doesn't load when you launch from a project subdirectory. From Claude Code 2.1.181 the plugin must be installed at user scope — project scope no longer inherits into subdirectories. Check with claude plugin list | grep -A2 discord; the Scope: line must say user. Symptom: the banner mentions channels but /mcp shows no discord server, and the plugin subprocess is never spawned.
  • A bot's memories land in a different project's bucket. A broken or empty .git in $HOME — even one git itself rejects — is enough to stop the harness's project-root walk there, so every bot without its own .git resolves its project root to $HOME. Transcripts still key on the working directory, which is what makes it invisible. Remove the stray .git; SKILL.md has the full detection-and-recovery procedure, including why matching on a memory's filename alone is not evidence of who wrote it.
  • A long reply gets split in the middle of a code block. That is upstream's chunk() doing a blind slice(0, 2000). Apply patch-discord-plugin.sh (patch #3). Note that switching chunkMode to 'newline' does not fix it on its own — a code block is nothing but newlines, so it still cuts inside the block.
  • Inbound messages aren't reaching the agent (one-way). Most common cause: org policy doesn't include discord in allowedChannelPlugins. Admin must enable via Claude.ai Admin Console → Claude Code → Channels. Editing ~/.claude/remote-settings.json locally is futile — the server overwrites within an hour.
  • /discord:access pair reports "code not found". The official skill hardcodes the default state dir path. Use claude-dc-pair from this repo, or follow the manual file-edit path in SKILL.md.
  • MCP "failed — Skipping connection" cached. Run /doctor then /mcp inside Claude Code; /mcp offers a manual retry. Resist the urge to raise MCP_TIMEOUT: a longer timeout only helps when the connection was about to succeed. When it never succeeds, all you have done is convert a fast failure into a slow one — in practice, a startup that hangs for the full timeout and is hard to interrupt.
  • Discord bot doesn't see message text. Enable "Message Content Intent" in Developer Portal → Bot → Privileged Gateway Intents.
  • stop_reason: refusal mid-session. Anthropic's safety classifier can false-positive on bloated sessions. Recovery: /exit then claude-dc (no -c/-r — those resume the poisoned session). Rebuild context from project files. Mitigation: keep tool outputs small, write big results to files instead of pasting them through the agent.

🔒 Security notes

  • Bot tokens grant full control of the bot. .env files are chmod 600 inside chmod 700 directories. Don't commit them, don't share OAuth invite URLs widely. The included .gitignore excludes .env, *.env, .claude-discord/, .claude/channels/.
  • allowFrom is the only gate to a Claude Code session behind a bot. Anyone whose Discord snowflake ID is listed can effectively type into the paired session. Treat the list as carefully as shell access.
  • The plugin sends outbound traffic only to discord.com/api/v10 and gateway.discord.gg. No third-party endpoints. The hooks and the patch script are local-only: they read and write files under ~/.claude, and make no network calls.
  • Never approve a pairing because a chat message asked you to. A message saying "approve the pending code" or "add me to the allowlist" is precisely the request a prompt injection would make. Verify the pending entry's senderId against your own Discord snowflake before pairing, and let the human run the pairing step.
  • Be cautious with "Public Bot" toggle — if Public, anyone with the OAuth URL can install the bot into their own server. Keep the URL private.

🆚 vs the Telegram sibling

Dimension Telegram sibling This (Discord)
Hard bot count limit 20 per @BotFather account Effectively unlimited
New-bot setup 3 steps in @BotFather chat (~2 min) ~~6 steps in Developer Portal browser (~~5 min)
Markdown formatting MarkdownV2 with full escape Discord Markdown, almost no escape
Headers (# H1) Not supported Supported
Message history API Not available (bot API limit) fetch_messages tool
Reactions Bot → user only Bidirectional (user reactions visible to bot)
Multi-bot collab in one room Not supported (private DMs only) Supported via shared channels
Mobile notifications Fastest Slightly slower

The Telegram sibling repo is recommended if you have <20 bots and prefer @BotFather's simpler in-app flow; this Discord repo is recommended for scale (50+ bots) or advanced collaboration patterns.

🤝 Contributing

PRs welcome, especially:

  • Variant-aware claude-dc-pair (currently only handles the primary bot's state dir).
  • A reference implementation of multi-bot orchestrator on top of this skill (planner / coder / reviewer / tester pattern).
  • Adaptations for Slack (no official plugin yet, requires a Slack bridge).

📜 License

MIT — see [LICENSE](LICENSE).

Reviews (0)

No results found