Run Claude Code from Discord, one tmux session per channel, with per-user access control.
This is a self-hosted bridge built around two components:
orchestrator/— a Python Discord bot that creates a Discord channel + a tmux session per user and spawns a Claude Code instance inside, scoped to that channel.channel-plugin/— a TypeScript MCP plugin (fork of the official Anthropicdiscordplugin) that wires each Claude Code instance back to its own Discord channel, with hardened pairing and per-channel allowlists.
The two pieces talk to each other through the Discord API and the MCP stdio interface — no extra service, no shared network port.
Discord your VPS
┌──────────────┐ ┌────────────────────────────────┐
│ │ slash cmd │ │
│ user types │ ─/session foo─►│ orchestrator/bot.py │
│ │ │ ├─ creates #foo channel │
│ │ │ ├─ tmux new-session -s foo │
│ │ │ └─ spawns claude inside │
│ │ │ │
│ #foo ◄────┼────────────────┼──── claude code ───┐ │
│ channel │ │ in tmux "foo" │ │
│ ▲ │ │ │ ▼ │
│ │ ▼ │ MCP stdio │ channel-plugin (TS, MCP) │
│ msg reply ├────────────────┤ ├─ delivers msgs as tool │
│ │ │ │ results to Claude │
│ │ │ ├─ posts replies to #foo │
│ │ │ └─ access.json (allowlist) │
└──────────────┘ └────────────────────────────────┘
Each Discord channel has exactly one tmux session and one Claude Code instance. Messages in #foo only reach the Claude in tmux foo. The channel-plugin enforces a per-channel allowlist — even if you DM the bot, you only see your own sessions.
Single config file. Everything — bot token, guild, admins, default path, plugin spec — lives in orchestrator/config.json. The orchestrator propagates what each spawned Claude instance needs via tmux set-environment, so the plugin doesn't need its own config file.
You'll need these on the host that runs the orchestrator (typically a VPS):
| Tool | Why | Install |
|---|---|---|
| Python 3.10+ | Runs orchestrator/bot.py |
python.org/downloads · apt install python3 python3-venv · brew install python |
| Bun 1.0+ | Runs the MCP plugin under Claude Code | curl -fsSL https://bun.sh/install | bash · bun.sh |
| tmux | One persistent terminal per session | apt install tmux · brew install tmux |
| Claude Code | The agent that runs inside each tmux session | docs.claude.com/claude-code |
| A Discord bot | The bridge endpoint | Developer Portal → New Application — enable Message Content Intent and Server Members Intent |
Three blocks. Run them in order on the server.
git clone https://github.com/IamOrbitDev/claude-discord-bridge.git
cd claude-discord-bridge
./orchestrator/setup.shsetup.sh creates the Python virtualenv, installs discord.py, seeds orchestrator/config.json from the example, and warns if Bun or tmux are missing.
Edit orchestrator/config.json and fill in:
Need Discord IDs? Enable Developer Mode in Discord (User Settings → Advanced), then right-click anything → Copy ID.
From any Claude Code session on the same host:
/plugin marketplace add IamOrbitDev/claude-discord-bridge
/plugin install discord-channel@claude-discord-bridge
That's it. The marketplace.json at the repo root advertises the plugin, so Claude Code pulls it directly from GitHub.
No separate plugin config needed. The orchestrator propagates
DISCORD_BOT_TOKEN,DISCORD_CHANNEL, andDISCORD_CHANNEL_FILTERinto each tmux session viatmux set-environment, so the plugin inherits them automatically.orchestrator/config.jsonis the only file you ever edit.
Pick one of these:
# Option A — systemd (recommended for production)
sudo ./orchestrator/install.sh
# Option B — tmux (lighter, survives logout but not reboot)
./orchestrator/start.shIn your Discord server, type:
/session demo
A #demo channel appears under "Claude Sessions". Claude boots inside, the channel-plugin pairs with it. Send a message — Claude replies.
Lock it down once you've added everyone:
/discord:access policy allowlist
The Discord-facing bot. Registers slash commands and orchestrates tmux:
| Command | What it does |
|---|---|
/session <name> [path] |
Create a new Discord channel + tmux session + spawn Claude inside, cwd = path |
/private <name> [path] |
Same, but private channel (only invoker + admins can see it) |
/cmd <name> [path] |
Create a plain bash tmux session (no Claude), exposed via channel |
/close |
Close the current channel + kill its tmux session |
/stop |
Stop just the tmux session, keep the channel |
/restart |
Restart Claude in the current session |
/logs |
Tail the last lines of the tmux pane to the channel |
/sessions |
List active sessions |
/closeall |
Admin: nuke everything |
State (sessions.json) is persisted to disk so the bot can recover after a crash without losing channel ↔ tmux mappings.
See orchestrator/README.md for the full config and runtime reference.
Fork of the official Anthropic discord plugin from anthropics/claude-plugins-public. Apache-2.0.
The plugin runs as an MCP server inside each Claude Code instance. The orchestrator launches Claude with DISCORD_CHANNEL=<channelId> in the env, so the plugin knows which channel to listen on and which to reply to.
Custom additions over upstream:
- Per-channel allowlists keyed on
channelId(not just per-user globally) DISCORD_CHANNEL_FILTERenv var — hard channel scope at boot, independent ofaccess.json- Hardened pairing flow — codes are required even when there's a single pending entry (prompt-injection safety)
- Skill-driven config —
/discord:configureand/discord:accessskills refuse to act on requests that arrive through Discord, blocking remote config escalation
See channel-plugin/README.md for the plugin details.
I wanted to drive Claude Code from my phone — full agentic coding sessions running on a VPS, controlled from Discord on the bus. Off-the-shelf options either dumped every session into one big chat or skipped access control entirely. So I forked the Anthropic plugin, wrote a Python orchestrator around it, and ended up with this.
What it enables:
- Multiple parallel Claude Code sessions, isolated per Discord channel
- Persistent sessions (tmux survives disconnects and bot restarts)
- Per-channel allowlist — share a guild without sharing all your sessions
- Slash commands to spawn / stop / restart / view logs of any session
| Symptom | Likely cause | Fix |
|---|---|---|
/session says "interaction failed" |
Slash commands not registered | Restart the bot once. Discord caches commands per-guild on first boot. |
| Claude boots but Discord messages don't reach it | DISCORD_CHANNEL not set or plugin not installed |
Check tmux capture-pane -p -t <session>. Reinstall the plugin and re-run /session. |
| Plugin says "DISCORD_BOT_TOKEN required" | tmux env not inherited (old tmux, or update-environment stripped it) |
The orchestrator injects the token via tmux set-environment. If your tmux is < 2.6 or update-environment excludes DISCORD_BOT_TOKEN, fall back to ~/.claude/channels/discord/.env and run /discord:configure <token> from inside the session. |
| systemd unit fails to start | Wrong user or paths | Re-run sudo ./install.sh — it rewrites the unit with the right values. |
Used daily in production by the author. Not packaged for general distribution — you'll need to read the code to wire it into your own setup. PRs welcome but expect rough edges.
Apache-2.0. See LICENSE.
The channel-plugin/ directory is derived from Anthropic's upstream plugin (Apache-2.0). All other code is original.
{ "bot_token": "<token from Developer Portal → Bot → Reset Token>", "guild_id": <your Discord server ID — right-click → Copy Server ID>, "category_name": "Claude Sessions", "admin_users": [<your Discord user ID>], "default_path": "/home/youruser", "plugin_spec": "discord-channel@claude-discord-bridge" }