Multi-platform bot (Telegram + Discord) that routes messages to Claude Code CLI subprocesses. Each chat/channel gets its own persistent Claude Code session. Runs on Max subscription (no API keys).
Telegram Cloud Discord Gateway
│ │
▼ (long polling) ▼ (websocket)
┌────────────────┐ ┌────────────────┐
│ grammY Bot │ │ Discord.js │
│ telegram-bot │ │ discord-bot │
└───────┬────────┘ └───────┬────────┘
│ │
▼ ▼
┌─────────────────────────────────┐
│ Platform Context (interface) │
│ - sendMessage, editMessage │
│ - sendTyping, sendFile │
│ - sendDraft (DM streaming) │
└───────────────┬─────────────────┘
│
▼
┌──────────────────┐
│ Message Queue │
│ - 3s debounce │
│ - mid-turn │
│ collect (20) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Session Manager │
│ - 1 per chat │
│ - LRU eviction │
│ - idle timeout │
│ - resume on │
│ respawn │
└────────┬─────────┘
│ spawns claude -p (stream-json)
▼
┌──────────────────┐
│ Claude Code CLI │
│ - per-agent │
│ workspace │
│ - model from │
│ config │
└────────┬─────────┘
│
▼
Anthropic API
Both platforms share one Session Manager and use the same stream-relay logic via the PlatformContext interface. Each platform provides an adapter that handles platform-specific message I/O (Telegram: grammY Context, Discord: discord.js Channel).
Message queue sits between platform bots and Session Manager. Rapid messages are debounced (3s window) into a single prompt. Messages arriving while Claude is processing are collected (up to 20) and delivered as a combined followup after the current turn completes.
Context injection: Each message includes metadata — current time, chat type (DM/group/topic), topic name, sender username, and emoji reactions. The agent knows where it is, when it is, and who it's talking to. Reactions are delivered as messages so the agent can respond to a thumbs-up or a ❤️ without the user typing anything.
Cron jobs run separately via launchd plists. Each plist calls run-cron.sh <task-name>, which invokes cron-runner.ts to spawn a one-shot claude -p session with the cron's prompt.
Config: config.yaml defines agents (workspace + model) and bindings (chatId/channelId -> agentId). User-specific overrides live in config.local.yaml (gitignored, deep-merged over config.yaml). At least one platform (Telegram or Discord) must be configured. Tokens are read from macOS Keychain at runtime.
- macOS (launchd required for bot service management)
- Node.js 20+ and npm
jq— required by hook scripts (brew install jq)- Claude Code CLI with Max subscription
- A Telegram bot token from @BotFather (or Discord bot token)
1. Clone and install
git clone https://github.com/fitz123/claude-code-bot.git ~/.minime
cd ~/.minime/bot && npm install2. Configure for your environment
config.yaml ships with working defaults. Create config.local.yaml for your overrides:
cp config.local.yaml.example config.local.yamlEdit config.local.yaml — set workspaceCwd to the absolute path of your repo and chatId to your Telegram user ID (send /start to @userinfobot to find it).
crons.yaml ships with example crons (all disabled). Create crons.local.yaml for your own crons:
cp crons.local.yaml.example crons.local.yamlCreate .claude/settings.local.json with required settings:
{
"outputStyle": "Your output style name",
"autoMemoryEnabled": true,
"autoMemoryDirectory": "/absolute/path/to/your/workspace/memory/auto"
}3. Store Telegram bot token in macOS Keychain
security add-generic-password -s 'telegram-bot-token' -a 'minime' -w 'YOUR_TOKEN_HERE'4. Store Claude Code OAuth token in Keychain
claude setup-token
# Copy the token, then store it:
security add-generic-password -s 'claude-code-oauth-token' -a 'minime' -w 'YOUR_OAUTH_TOKEN'The bot reads this token at startup via start-bot.sh and run-cron.sh — it does not use claude auth login.
5. Create launchd service
mkdir -p ~/.minime/logs
cp bot/telegram-bot.plist.example ~/Library/LaunchAgents/ai.minime.telegram-bot.plistEdit the plist — replace WORKSPACE, LOG_DIR, and USER_HOME with your paths.
6. Validate and start
cd ~/.minime && npx tsx bot/src/config.ts --validate
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.minime.telegram-bot.plist7. Verify
launchctl list | grep ai.minime.telegram-bot
tail -f ~/.minime/logs/telegram-bot.stdout.logSend a message to your bot in Telegram to confirm it responds.
Discord: Store token in Keychain (security add-generic-password -s 'discord-bot-token' -a 'minime' -w 'TOKEN'), add a discord section to config.local.yaml. See config.yaml for full reference.
Crons: Add your crons to crons.local.yaml (copy from crons.local.yaml.example), then generate and load plists:
cd ~/.minime/bot && npx tsx scripts/generate-plists.ts
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.minime.cron.<name>.plistOptional rules: cp .claude/optional-rules/memory-protocol.md .claude/rules/custom/
ADR governance: mkdir -p reference/governance && cp reference/governance/decisions.md.example reference/governance/decisions.md
The bot runs as a launchd service: ai.minime.telegram-bot.
# Check status
launchctl print gui/$(id -u)/ai.minime.telegram-bot 2>&1 | head -5
# Restart (graceful — waits for active sessions to finish)
launchctl kill SIGTERM gui/$(id -u)/ai.minime.telegram-bot
# Stop
launchctl bootout gui/$(id -u)/ai.minime.telegram-bot
# Start (if stopped)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.minime.telegram-bot.plistWarning: Graceful restart sends SIGTERM — the bot injects a shutdown message into active sessions and waits up to 60s for turns to complete before exiting. Idle sessions close immediately. launchd auto-restarts via KeepAlive. Still, active work is interrupted — always confirm before restarting.
-
Edit
crons.local.yaml— add a new entry:- name: my-task schedule: "30 9 * * *" prompt: > Do the thing. agentId: main deliveryChatId: YOUR_CHAT_ID
Cron field reference:
Field Type Required Description namestring yes Unique identifier for the cron job schedulestring yes 5-field cron expression, local timezone type"llm"or"script"no "llm"(default) runsclaude -p;"script"runs a shell commandpromptstring for llm Prompt sent to Claude commandstring for script Shell command to execute agentIdstring yes Must match an agent in config.yamlorconfig.local.yamldeliveryChatIdnumber no Telegram chat ID for delivery (falls back to config default) deliveryThreadIdnumber no Telegram forum topic ID for delivery timeoutnumber no Per-cron timeout in ms (default: 300000 = 5 min) enabledboolean no Set falseto disable without deleting (default:true) -
Generate launchd plists:
cd ~/.minime/bot && npx tsx scripts/generate-plists.ts
-
Load and test:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.minime.cron.my-task.plist launchctl start ai.minime.cron.my-task tail -f ~/.minime/logs/cron-my-task.log
To remove: launchctl bootout gui/$(id -u)/ai.minime.cron.<name>, delete from crons.local.yaml, regenerate.
-
Add an agent and binding to
config.local.yaml:agents: new-agent: id: new-agent workspaceCwd: /Users/YOU/.minime/workspace-new model: claude-opus-4-6 bindings: - chatId: 123456789 agentId: new-agent kind: dm label: New Agent DM
See config.yaml for all binding options including
requireMention,voiceTranscriptEcho,typingIndicator, and per-topic overrides for forum supergroups. -
Validate and restart:
cd ~/.minime/bot && npx tsx src/config.ts --validate launchctl kill SIGTERM gui/$(id -u)/ai.minime.telegram-bot
-
Store the Discord bot token in macOS Keychain:
security add-generic-password -s 'discord-bot-token' -a 'minime' -w 'YOUR_TOKEN_HERE'
-
Add the
discordsection toconfig.local.yaml:discord: tokenService: discord-bot-token bindings: - guildId: "9876543210" agentId: main kind: channel label: My Server requireMention: true
See config.yaml for per-channel overrides and guild-wide defaults.
-
Required bot permissions/intents: Guilds, GuildMessages, MessageContent (privileged), DirectMessages. Slash commands (
/start,/reconnect,/clean,/status) are registered per-guild on startup.
telegramTokenService is optional — the bot can run Discord-only.
All log output uses structured format: TIMESTAMP LEVEL [tag] message.
| Setting | Type | Default | Description |
|---|---|---|---|
logLevel (config.yaml) |
string | "info" |
Log verbosity: debug, info, warn, error |
LOG_LEVEL (env var) |
string | — | Overrides logLevel from config when set |
When metricsPort is set in config.yaml, the bot exposes a Prometheus-compatible /metrics endpoint on 127.0.0.1 at that port.
metricsPort: 9090See bot/src/metrics.ts for the full list of exported metrics.
Older versions shipped config.yaml.example which you copied to config.yaml (gitignored). The current version tracks config.yaml directly and uses config.local.yaml for user overrides.
If you have a local config.yaml from the old workflow, git will refuse to pull because the file is now tracked. Migrate before pulling:
# 1. Back up your current config
cp config.yaml config.local.yaml
# 2. Remove the untracked file so git can check out the new tracked version
rm config.yaml
# 3. Pull — git will restore config.yaml with upstream defaults
git pull
# 4. Edit config.local.yaml — keep only your overrides (workspaceCwd, chatId, tokens, bindings)
# Remove anything that matches the upstream defaults in config.yamlYour config.local.yaml is deep-merged over config.yaml at startup, so you only need to keep what differs from the defaults.
Most Telegram bots for Claude use the Agent SDK, which requires API keys and falls under Anthropic's Commercial Terms. Anthropic explicitly prohibits using Max/Pro subscription OAuth tokens through the Agent SDK:
OAuth authentication (used with Free, Pro, and Max plans) is intended exclusively for Claude Code and Claude.ai. Using OAuth tokens in any other product, tool, or service -- including the Agent SDK -- is not permitted.
This bot spawns the original claude -p binary directly. Same CLI you run in your terminal. Max subscription, no API keys, no per-token billing.
| Project | Engine | Max-compliant |
|---|---|---|
| claude-code-bot (this) | CLI binary (claude -p) |
Yes |
| PleasePrompto/ductor | CLI binary (subprocess) | Yes |
| Anthropic Official Plugin | MCP extension of active CC session | Yes |
| RichardAtCT/claude-code-telegram | Agent SDK (claude_agent_sdk) |
No |
| earlyaidopters/claudeclaw | Agent SDK (@anthropic-ai/claude-agent-sdk) |
No |
| linuz90/claude-telegram-bot | Agent SDK (@anthropic-ai/claude-agent-sdk) |
No |
| NachoSEO/claudegram | Agent SDK (@anthropic-ai/claude-agent-sdk) |
No |
| openclaw/openclaw | Own agent runtime | No (API keys) |
| qwibitai/nanoclaw | Claude Code in Docker | No (API keys) |
| mtzanidakis/praktor | Agent SDK in Docker | No (API keys) |
| six-ddc/ccbot | tmux bridge (CLI in tmux) | Yes |
| chenhg5/cc-connect | Bridge/proxy | Depends on agent |
Four projects run the actual CLI binary on a Max subscription without API keys: this bot, ductor, ccbot, and the official plugin.
The official plugin is an MCP server that adds Telegram tools to an already-running Claude Code session.
- Not a standalone bot. Requires an active Claude Code session on your computer. Close the lid and it stops
- No cron or scheduled tasks. No autonomous work while you're away
- Single session. No parallel workspaces, no multi-agent
- Supports group chats but not forum topic routing
- No workspace health management or memory consolidation
It's a remote control for your terminal session, not an autonomous bot.
ccbot runs Claude Code inside tmux and bridges it to Telegram via two channels: JSONL transcript polling for content, and terminal scraping for interactive UI.
What ccbot does better: tool use visibility (which tool was called, what it returned), thinking content as expandable blockquotes, and interactive permission handling — approve or deny tool calls from Telegram via inline keyboard. These are real advantages that claude -p stream-json cannot provide today.
The trade-off is fragility. Hardcoded regex patterns match Claude Code's terminal UI text — prompt wordings, spinner characters, chrome separators. Any Claude Code TUI update can silently break detection. Input goes through send_keys() with empirical timing delays. Two polling loops (JSONL at 2s + terminal scrape at 1s per window) add overhead that scales linearly with sessions.
No cron system, no multi-agent, no workspace management, no Discord. Single-user remote control with excellent visibility into what Claude is doing.
Ductor is the closest alternative. Also spawns the CLI binary, also ToS-compliant, also supports forum topics.
| claude-code-bot | ductor | |
|---|---|---|
| Language | TypeScript (grammY) | Python (aiogram) |
| Codebase | ~3k LoC | ~150 modules |
| Forum topic sessions | Yes | Yes |
| Multi-agent with isolated workspaces | Yes | Yes |
| Cron system | launchd plists (per-cron process isolation) | In-process scheduler |
| Crash safety | Atomic JSON writes, launchd auto-restart | Atomic writes, in-flight turn tracking, process registry |
| Workspace health | Filesystem guardian hooks + structural audits | Agent health with exponential backoff |
| Memory consolidation | Nightly summarization cron | File sync |
| Platforms | Telegram + Discord | Telegram + Matrix |
| Multi-CLI support | Claude Code | Claude Code, Codex, Gemini |
Neither project is strictly better than the other — feature sets are comparable. Ductor covers more CLIs and has deeper crash recovery (in-flight turn tracking, process registry, stream coalescing). We're significantly simpler: a thin TypeScript wrapper around claude -p that delegates complexity to the OS (launchd for process isolation, filesystem hooks for workspace protection) rather than reimplementing it in application code.

