Talk to Claude Code
from a Discord channel
discord-bridge is a small, single-file Python bot that pipes one Discord channel into a resident AI coding agent โ Claude Code by default, or a pluggable backend of your choice. Every allowed message streams a reply back by editing a placeholder message โ no server, no database, just an opaque piece of state persisted between messages.
Features
Pluggable backends
Claude Code by default; another agentic CLI or any OpenAI-compatible chat API (grok, GLM, local models) via one env var.
Streaming replies
The placeholder message is edited live as output arrives โ not dumped all at once at the end.
Image attachments
Claude/CLI backends see them by file path (their own Read tool); openai_compatible sees inline base64, if the model supports vision.
Persistent conversation
Messages in the channel are turns in one ongoing conversation, not one-shot prompts.
Hard allowlist
Wrong channel or wrong Discord user id, and the message never reaches the backend โ everyone else gets a ๐ซ reaction.
!reset / !new
Clear the conversation and start over, from Discord, without SSH.
Self-healing
A stale or invalid session is detected from the error text and cleared automatically.
Watchdog timeout
A hung backend is killed after BRIDGE_TIMEOUT_SECONDS instead of hanging forever.
Clean shutdown
SIGTERM/SIGINT kill any in-flight subprocess and close the gateway gracefully โ systemctl restart doesn't orphan anything.
No permission bypass
Claude/CLI backends never run with --dangerously-skip-permissions; anything outside your permissions.allow just fails.
Requirements
- Python 3.10+
- A Discord application + bot token with the Message Content privileged intent enabled, invited to your server with permission to view/send/manage messages in one channel
- Whatever the chosen backend needs โ see below
Backends
One instance uses exactly one backend, chosen with BACKEND. Running more than one model is running more than one instance (see Running multiple agents) โ there's no per-message model switching.
| BACKEND | Runs | Tool access | Persistence |
|---|---|---|---|
| claude (default) | claude -p --resume | Yes, via permissions.allow | Server-side resumable session |
| cli | Another agentic CLI you configure (Codex, Gemini CLI, ...) | Yes, whatever that CLI grants | Whatever that CLI's resume flag supports โ some have none |
| openai_compatible | Any POST {base_url}/chat/completions endpoint โ grok, GLM, a local model via Ollama/vLLM/llama.cpp | No โ chat only | A local transcript this bridge maintains, truncated to OPENAI_COMPATIBLE_MAX_HISTORY |
openai_compatible is chat only โ no file edits, no shell commands. cli's example configs for Codex/Gemini CLI in .env.example are researched but not verified end-to-end here (neither CLI is installed on the machine this was built on) โ confirm against your own install's --help before deploying. See ADR-0005.
Quickstart
git clone https://github.com/supachai-j/discord-bridge.git
cd discord-bridge
python3 -m venv venv
venv/bin/pip install -r requirements.txt
cp .env.example .env
$EDITOR .env # DISCORD_CHANNEL_ID, DISCORD_ALLOWED_USER_IDS, etc.
install -m 600 /dev/stdin ~/.discord_bot_token <<< "<your bot token>"
venv/bin/python bridge.py # run it directly to test before wiring up systemd
Send a message in the channel you configured. If nothing happens, check DISCORD_ALLOWED_USER_IDS matches your Discord user id and that the bot has the Message Content intent enabled.
Run as a service
[Unit]
Description=Discord bridge for Claude Code
After=network-online.target
Wants=network-online.target
# Give up after 5 restarts in 5 minutes instead of hammering Discord's
# login endpoint forever on something that won't self-resolve.
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=simple
User=youruser
WorkingDirectory=/home/youruser/discord-bridge
EnvironmentFile=/home/youruser/discord-bridge/.env
Environment=PYTHONUNBUFFERED=1
ExecStart=/home/youruser/discord-bridge/venv/bin/python /home/youruser/discord-bridge/bridge.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now discord-bridge.service
journalctl -u discord-bridge.service -f
Running multiple agents
One process serves one channel with one session. To run several agents side by side โ different personas, different channels, even different Discord applications โ use a systemd instantiated unit: one template, one process per instance, full isolation.
[Service]
WorkingDirectory=/home/youruser/discord-bridge
EnvironmentFile=/home/youruser/discord-bridge/instances/%i.env
ExecStart=/home/youruser/discord-bridge/venv/bin/python /home/youruser/discord-bridge/bridge.py
Restart=on-failure
cp instances/instances.example.env instances/myagent.env
$EDITOR instances/myagent.env
sudo systemctl enable --now discord-bridge@myagent.service
Add another agent by repeating the last two lines with a new instances/<name>.env โ the template and the code are shared. Two instances can even share one bot token (each just opens its own gateway session) as long as BRIDGE_SESSION_FILE differs between them.
Configuration
Everything is read from the environment โ see .env.example for the full commented list. Nothing deployment-specific is hardcoded in bridge.py.
| Variable | Required | Default | Controls |
|---|---|---|---|
| DISCORD_BOT_TOKEN_FILE | no | ~/.discord_bot_token | path to the bot token |
| DISCORD_CHANNEL_ID | yes | โ | the one channel the bot listens in |
| DISCORD_ALLOWED_USER_IDS | yes | โ | comma-separated Discord user ids allowed to command it |
| BACKEND | no | claude | claude / cli / openai_compatible โ see Backends |
| CLAUDE_BIN | no | claude | path to the claude CLI โ use a full path under systemd (BACKEND=claude) |
| CLAUDE_MODEL / CLAUDE_FALLBACK_MODEL | no | claude's default | which model answers, plus an automatic fallback (BACKEND=claude) |
| CLAUDE_EFFORT | no | claude's default | low / medium / high / xhigh / max (BACKEND=claude) |
| CLAUDE_CONFIG_DIR | no | claude's default | run under a dedicated identity/permissions โ see Security |
| BRIDGE_WORKDIR | no | ~ | cwd for a subprocess backend โ keep off $HOME |
| BRIDGE_SESSION_FILE | no | ~/discord-bridge/session_id.txt | where the backend's opaque state lives |
| BRIDGE_TIMEOUT_SECONDS | no | 1200 | kill a hung backend after this long |
| CLI_BIN / CLI_ARGS_NEW / CLI_ARGS_RESUME | for cli | โ | the other CLI's binary and argv templates |
| OPENAI_COMPATIBLE_BASE_URL / _API_KEY_FILE / _MODEL | for openai_compatible | โ | endpoint, key file, model name |
Security model
Three gates, outside-in:
- Channel โ messages outside
DISCORD_CHANNEL_IDare ignored. - User โ the author must be in
DISCORD_ALLOWED_USER_IDS, or the message never reaches Claude (just a ๐ซ reaction). - Tool permissions โ whatever
permissions.allow/.denyis set in the targetCLAUDE_CONFIG_DIR'ssettings.jsonapplies as normal. The bridge never passes--dangerously-skip-permissionsor a permissive--permission-mode.
Add explicit Read() deny rules for ~/.ssh, your bot token file, and any credentials โ regardless of BRIDGE_WORKDIR, since Read() rules match by path, not by cwd.
Leaving CLAUDE_CONFIG_DIR unset means the bot runs under the same permissions as your own interactive claude sessions โ typically much broader than a bot needs. Set it to a dedicated config dir.
Development
pip install -r requirements-dev.txt
ruff check .
pytest -q
CI runs both on every push and pull request to main. Tests cover the pure logic โ config parsing, stale-session detection, chunking โ not the Discord/claude integration itself; that's verified by restarting the service and sending a real message.
Releasing
git tag v0.2.0
git push --tags
A GitHub Release with auto-generated notes is created automatically. That's the entire release process โ there is no CD. Getting a release onto a running instance is a deliberate manual step: git pull && sudo systemctl restart discord-bridge.service. A bad deploy should require a human to have typed that.
Known limitations
- One conversation for the whole channel โ not one per Discord user.
- One channel per process โ see Running multiple agents for more than one.
- Messages are not queued โ a second message while one is running gets a "busy" reply.
- No integration test suite โ the Discord/claude interaction itself is verified manually.
FAQ
Why not just use the claude Discord/Slack integrations?
Those run Claude in Anthropic's infrastructure. This runs claude as a subprocess on your machine, under whatever tool permissions you configure โ useful when the point is letting Claude act on files, run commands, or talk to services that only exist on your box.
Can more than one person use it?
Not out of the box โ DISCORD_ALLOWED_USER_IDS can list several ids, but they'd all share one conversation and one BRIDGE_SESSION_FILE. For genuinely separate agents, run separate instances (see Running multiple agents).
What happens if claude hangs?
A watchdog thread kills it after BRIDGE_TIMEOUT_SECONDS (default 20 minutes) and the channel gets an error reply instead of silence.
Does it work with vCenter/ESXi, bare metal, a Raspberry Pi?
Anywhere Python 3.10+ and claude can run โ it's one file with two dependencies (discord.py and whatever claude needs). systemd is assumed for the service examples but isn't required to run bridge.py directly.