Talking to the model was the small part. Most of the work was keeping a long-running bot from doing something dumb.
Claude Discord is a self-hostable Claude bot for a private Discord server. Each process is one bot with its own Discord token, working directory, persona, and conversation state. You can run several in the same server and they can hand work to each other. Here are the parts that took real effort.
Why Discord
A Discord server already has channels to separate work, clients on every device I own, and permissions that answer who is allowed to talk to the bot. Inside a channel the bot keeps a live Claude session instead of starting fresh on each message, so a channel reads like a conversation instead of a series of one-off prompts.
Filtering before the model
Three checks run before any message costs tokens. First, drop the bot's own messages, webhook-forged messages, and Discord system messages. Second, an owner allowlist; the process refuses to start without a valid owner user ID. Third, an engagement rule: reply to mentions anywhere, reply to anything in a channel the bot has chosen to watch, and reply to other bots only under a cooldown.
Restarts
Early on, restarting a bot was risky. To rebuild context it replayed recent channel history, and then it treated that history as new work and answered requests from days earlier instead of the message that had just tagged it. The fix was a persisted watermark. On restart the history is split at the watermark: everything at or before the last reply is labelled as background, and only messages after it are treated as work.
Bots talking to bots
With more than one bot in a server they show up in the member list, mention each other by name, and a bot pulled into a channel by a peer will answer there. Left alone, two of them will reply to each other forever. So a bot-to-bot exchange stops after six consecutive turns, pauses, and pings the owner. Any human message resets the count.
Session memory
Each channel gets its own ClaudeSDKClient, and each one uses about 190 MB while warm. An LRU pool keeps three alive and evicts the least recently used one under memory pressure. The one rule I had to add: never evict a session that has a turn in progress.
Discord tools for the model
An in-process MCP server gives the model tools for Discord itself: read any channel's history, start or stop watching a channel (the watched set survives restarts), post into another channel, and create, rename, move, or delete channels. This is what lets me say "make a channel for the icon work and move this there" and have that be the whole instruction.
Smaller things
The bot posts a "working" placeholder and edits the answer into it as it streams. Long answers are split at Discord's 2,000-character limit and attached as a file past 6,000. /stop interrupts the current turn but keeps the session, so context survives a cancel. Auth is an OAuth token from claude setup-token, so usage bills against a Claude plan instead of per-token API credit.
Stack: Python, discord.py, and the Claude Agent SDK. Each instance is one process on a host you control.
Originally published at jackhomer.com/writing/claude-bots-in-discord/ by Jack Homer.
Top comments (0)