/>discord-bridge GitHub โ†—
MIT LICENSED ยท SELF-HOSTED ยท NO CLOUD DEPENDENCY

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.

BACKENDRunsTool accessPersistence
claude (default)claude -p --resumeYes, via permissions.allowServer-side resumable session
cliAnother agentic CLI you configure (Codex, Gemini CLI, ...)Yes, whatever that CLI grantsWhatever that CLI's resume flag supports โ€” some have none
openai_compatibleAny POST {base_url}/chat/completions endpoint โ€” grok, GLM, a local model via Ollama/vLLM/llama.cppNo โ€” chat onlyA local transcript this bridge maintains, truncated to OPENAI_COMPATIBLE_MAX_HISTORY
Not the same as codex/Gemini CLI's own agentic ability

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

shell
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

/etc/systemd/system/discord-bridge.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
shell
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.

/etc/systemd/system/discord-bridge@.service
[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
shell
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.

VariableRequiredDefaultControls
DISCORD_BOT_TOKEN_FILEno~/.discord_bot_tokenpath to the bot token
DISCORD_CHANNEL_IDyesโ€”the one channel the bot listens in
DISCORD_ALLOWED_USER_IDSyesโ€”comma-separated Discord user ids allowed to command it
BACKENDnoclaudeclaude / cli / openai_compatible โ€” see Backends
CLAUDE_BINnoclaudepath to the claude CLI โ€” use a full path under systemd (BACKEND=claude)
CLAUDE_MODEL / CLAUDE_FALLBACK_MODELnoclaude's defaultwhich model answers, plus an automatic fallback (BACKEND=claude)
CLAUDE_EFFORTnoclaude's defaultlow / medium / high / xhigh / max (BACKEND=claude)
CLAUDE_CONFIG_DIRnoclaude's defaultrun under a dedicated identity/permissions โ€” see Security
BRIDGE_WORKDIRno~cwd for a subprocess backend โ€” keep off $HOME
BRIDGE_SESSION_FILEno~/discord-bridge/session_id.txtwhere the backend's opaque state lives
BRIDGE_TIMEOUT_SECONDSno1200kill a hung backend after this long
CLI_BIN / CLI_ARGS_NEW / CLI_ARGS_RESUMEfor cliโ€”the other CLI's binary and argv templates
OPENAI_COMPATIBLE_BASE_URL / _API_KEY_FILE / _MODELfor openai_compatibleโ€”endpoint, key file, model name

Security model

Three gates, outside-in:

  • Channel โ€” messages outside DISCORD_CHANNEL_ID are 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/.deny is set in the target CLAUDE_CONFIG_DIR's settings.json applies as normal. The bridge never passes --dangerously-skip-permissions or a permissive --permission-mode.
If you don't fully trust the allowed user's machine

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

shell
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

shell
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.