CEO Agent Tools Channels
Claude channels for multi-agents
Ask AI about CEO Agent Tools Channels
Powered by Claude Β· Grounded in docs
I know everything about CEO Agent Tools Channels. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
CEO Agent Tools & Channels
Fork of the official Anthropic Telegram Channel Plugin, extended for multi-agent workflows.
Multi-agent toolkit for startup founders. Manage isolated Claude Code agents through separate Telegram chats β each with its own skills, tools, and personality.
Author: Roman Belopolskiy, CEO 4sell.ai
What's different from the official plugin
The official telegram@claude-plugins-official plugin stores the bot token globally β one token per machine. This project solves that:
| Official plugin | This project | |
|---|---|---|
| Bot token | Global (one per machine) | Per-bot registry (~/.claude/telegram-bots.json) |
| Multiple bots | Not supported | Run N bots in parallel |
| Runtime | Bun | Node.js >= 18 |
| Telegram lib | Grammy | Pure fetch (zero deps) |
| Config | /telegram:configure | claude-tg launcher with interactive bot selection |
| Isolation | Shared state | Each agent has its own context |
| Process cleanup | Zombie processes on crash | Auto-exit on stdin close / signals |
| Pairing | Per-session | Per-bot persistent access lists |
| Authorization | Pairing flow only | Pairing flow OR direct allowlist file (no interaction) |
| Media support | Text only | Photos and documents (saved to /tmp/, path forwarded to agent) |
| Agent creation | Manual | skills/spawn-agent β automated full setup |
Why this exists
Running a startup means juggling SMM, development, analytics, ops β all at once. AI agents help, but managing them through a single Claude Code terminal or a cluttered web UI is painful. Too many context switches, too many wasted tokens.
This project was born out of frustration with managing agents through a single interface. The idea is simple: one Telegram chat = one agent with its own isolated context. You text your SMM bot β it writes posts. You text your dev bot β it ships code. No cross-contamination, no token waste, no cognitive overhead.
Built on top of Claude Code Channels β a feature that lets external systems push messages into a running Claude Code session.
How it works
You (Telegram)
β
βββ @smm_bot βββΊ Claude Code session #1 (CLAUDE.md: SMM skills)
βββ @dev_bot βββΊ Claude Code session #2 (CLAUDE.md: frontend dev)
βββ @senior_dev_bot βββΊ Claude Code session #3 (CLAUDE.md: architecture + deploy)
Under the hood (SSE mode β recommended):
βββ Claude Code session #1 (?bot=smm)
Telegram Bot API βββ long polling ββ SSE Server ββΌββ Claude Code session #2 (?bot=devops)
ββ send_message βββΊ (port 3200) βββ Claude Code session #3 (?bot=senior)
- A single shared SSE server polls all Telegram bots and routes messages
- Each Claude Code session connects via SSE URL with
?bot=NAMEβ receives only messages for its bot - When you send a message in Telegram, it's routed to the correct agent session
- Typing indicator β Telegram shows "typing..." while the agent is processing your message
- Bots sharing the same token are polled once (no 409 Conflict errors)
- Permission requests (tool approvals) are forwarded to Telegram β approve or deny from your phone
Stdio mode is still supported for single-bot setups (backward compatible).
Quick start
1. Create a Telegram bot
Open @BotFather in Telegram, run /newbot, and save the token.
2. Install
git clone https://github.com/romanbelopolskiy/CEO-agent-tools-channels.git
cd CEO-agent-tools-channels
npm install
npm run build
3. Start the SSE server
PORT=3200 TRANSPORT=sse node dist/index.js
Or install as a launchd service (macOS β auto-start + keepalive):
cp examples/com.ceo-agent-tools.channels-sse.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.ceo-agent-tools.channels-sse.plist
Verify: curl http://127.0.0.1:3200/health
4. Configure your agent
Add to your agent's .mcp.json:
{
"mcpServers": {
"ceo-agent-tools-channels": {
"type": "sse",
"url": "http://127.0.0.1:3200/sse?bot=devops"
}
}
}
5. Launch the agent
cd ~/agents/devops
claude-tg
# Auto-detects bot name from directory, connects to SSE server
Or use claude-tg --bot devops from any directory.
Alternative: Interactive mode β run claude-tg and pick a bot from the list.
tmux requirement for
/stop:claude-tgmust run inside a tmux session whose name matches the bot name (e.g.tmux new-session -s devops). The/stopcommand sends ESC viatmux send-keysβ it has no effect if there is no matching tmux session.
launchd PATH requirement (macOS Apple Silicon)
If you run the SSE server via launchd, add /opt/homebrew/bin to EnvironmentVariables in your plist (~/Library/LaunchAgents/com.ceo-agent-tools.channels-sse.plist). launchd's default PATH does not include Homebrew, so tmux (needed for /stop) is not resolvable without it:
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<!-- other vars -->
</dict>
Without this fix, /stop fails with ENOENT when trying to call tmux.
5. Authorize your Telegram account
There are two ways to authorize yourself:
Option A β Interactive pairing (default)
- Send any message to your bot in Telegram
- The bot replies with a 6-character pairing code
- In Claude Code, say:
pair code abc123 - The bot sends a confirmation: "Bot authorized under name devops"
- Done β your messages now reach Claude Code
Option B β Direct allowlist (no pairing required)
Faster for multi-agent setups where you're creating many bots at once. Bypass the pairing flow by writing the access file directly:
cat > ~/.claude/telegram-access-{botname}.json << 'EOF'
{
"policy": "allowlist",
"allowedUsers": [YOUR_TELEGRAM_USER_ID],
"pendingPairs": {}
}
EOF
Replace {botname} with your bot name from the registry (e.g. devops, smm) and YOUR_TELEGRAM_USER_ID with your Telegram user ID.
Finding your user ID: send a message to @userinfobot β it replies with your ID.
This file is read at startup. No restart needed if the agent isn't running yet β just create the file before launching.
Bot registry
Bots are stored in ~/.claude/telegram-bots.json:
{
"devops": { "token": "123456:ABC..." },
"smm": { "token": "789012:DEF..." },
"senior": { "token": "345678:GHI..." }
}
Each bot gets its own access list at ~/.claude/telegram-access-{name}.json.
You can add bots manually to the JSON file or let claude-tg prompt you on first run.
Multi-agent setup
The core use case: run multiple agents from separate directories, each with its own Telegram bot, personality, and toolset.
Directory structure
my-agents/
βββ smm-bot/
β βββ CLAUDE.md β "You are an SMM manager..."
β βββ .claude/skills/ β create-post, write-comment, find-accounts
βββ dev-bot/
β βββ CLAUDE.md β "You are a frontend developer..."
β βββ .claude/skills/ β fix-bug, update-page
βββ dev-senior/
βββ CLAUDE.md β "You are a senior engineer..."
βββ .claude/skills/ β refactor, deploy, review-pr
Launch each agent
# Terminal 1 β SMM agent
cd smm-bot && claude-tg # select "smm"
# Terminal 2 β Dev agent
cd dev-bot && claude-tg # select "devops"
# Terminal 3 β Senior dev agent
cd dev-senior && claude-tg # select "senior"
Each session reads its own CLAUDE.md and .claude/skills/ β fully isolated contexts, zero token leakage between agents. The bot token is resolved from the shared registry by name.
Manual setup (without claude-tg)
SSE mode (recommended):
- Start the SSE server (see step 3 above)
- Add to your agent's
.mcp.json:
{
"mcpServers": {
"ceo-agent-tools-channels": {
"type": "sse",
"url": "http://127.0.0.1:3200/sse?bot=devops"
}
}
}
- Launch Claude Code:
claude --dangerously-load-development-channels server:ceo-agent-tools-channels
Stdio mode (single-bot, legacy):
{
"mcpServers": {
"ceo-agent-tools-channels": {
"command": "node",
"args": ["/absolute/path/to/CEO-agent-tools-channels/dist/index.js"],
"env": {
"TELEGRAM_BOT_NAME": "devops"
}
}
}
}
Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
TRANSPORT | no | stdio | Transport mode: sse or stdio. Auto-set to sse if PORT is set. |
PORT | no | 3200 | Port for SSE server (only in SSE mode) |
TELEGRAM_BOT_NAME | no | β | Bot name filter (stdio mode only). In SSE mode, use ?bot=NAME query param instead. |
TELEGRAM_POLL_INTERVAL | no | 1000 | Polling interval in ms |
DEBUG | no | 0 | Set to 1 to enable debug logging to stderr |
MCP_LOG_FILE | no | β | Path to a file for persistent debug logging |
TELEGRAM_GROUP_POLICY | no | mention-only | How to handle group chat messages: open | allowlist | mention-only |
Bot routing
SSE mode: Each Claude Code session connects to http://host:port/sse?bot=NAME. The server routes messages from that bot only to that session. Without ?bot=, the session receives messages from all bots.
Stdio mode: Set TELEGRAM_BOT_NAME env var β only that bot is loaded and polled.
Token deduplication
If multiple bot names share the same token (e.g., post/smm are aliases for the same Telegram bot), the server polls once per unique token and delivers messages to all alias sessions. This prevents Telegram 409 Conflict errors.
SSE Health endpoint
curl http://127.0.0.1:3200/health
# {"status":"ok","sessions":11,"bots":["devops","smm",...],"typing":2}
Typing indicator
In SSE mode, when a Telegram message is forwarded to an agent, the server automatically sends typing action to the chat. The indicator repeats every 4 seconds and stops when the agent replies via send_telegram_message (or after 2-minute timeout).
Tools
send_telegram_message
Send a message to a Telegram chat.
| Parameter | Type | Description |
|---|---|---|
chat_id | number | Chat ID (from channel event metadata) |
text | string | Message text (Markdown) |
Interrupt commands (user-initiated, not tools)
These are human-operator commands intercepted by the MCP server before reaching the agent. All three require claude-tg to run inside a tmux session named after the bot.
| Command | Trigger patterns | What happens |
|---|---|---|
/stop | stop, /stop, ΡΡΠΎΠΏ, esc, escape | Sends Escape to the CLI β cancels the current turn |
/status | status, /status, ΡΡΠ°ΡΡΡ | Sends Escape, waits 150ms, then types /status Enter β prints current session status. Live output streams back to Telegram while command runs. |
/compact | compact, /compact, ΠΊΠΎΠΌΠΏΠ°ΠΊΡ | Sends Escape, waits 150ms, then types /compact Enter β compacts the conversation context. Live output streams back to Telegram while command runs. |
How it works: for /stop, the server sends tmux send-keys -t <botName> Escape. For /status and /compact, it sends Escape first (to bring the CLI prompt back if mid-inference), waits 150ms, then types the command. All three finalize the active status message and stop the "typingβ¦" indicator immediately.
Constraints:
claude-tgmust be running inside a tmux session named after the bot (e.g. sessiondevopsfor botdevops).- If the session doesn't exist, the bot replies: "No tmux session '' β claude-tg not running".
- launchd plist must include
/opt/homebrew/binon PATH (see Setup, step 5).
For agents: do NOT use these programmatically. These are human interrupts β only the operator should send them from Telegram.
telegram_access
Manage access control.
| Parameter | Type | Description |
|---|---|---|
action | string | pair, unpair, list, or set-policy |
code | string | Pairing code (for pair) |
user_id | number | User ID (for unpair) |
policy | string | open or allowlist (for set-policy) |
Group chat support
Agents can be added to Telegram group chats and will respond based on the TELEGRAM_GROUP_POLICY setting.
Policies
| Policy | Behavior |
|---|---|
mention-only | Only respond when bot is @mentioned or the message is a reply to a bot message. Default. Ideal for finance, ops, or any shared team chat where the bot should stay quiet unless addressed. |
allowlist | Only respond to messages from users in the access list. Useful for bots where the group is shared but only admins should trigger it. |
open | Respond to all messages in the group, same as DM behavior. Use only for dedicated bot channels with no casual conversation. |
What agents receive
When a message arrives from a group, Claude Code receives the full context via channel metadata:
chat_type = "supergroup"
chat_title = "Finance Team"
is_group = "true"
bot_mentioned = "true" β bot was @mentioned
is_reply_to_bot = "false"
This lets agents make their own filtering decisions in CLAUDE.md on top of the MCP-level policy.
Bot mention detection
Mention is detected via:
@botusernameappearing in text (case-insensitive)- Telegram
mentionentity pointing to the bot text_mentionentity (for bots without usernames)- Message is a reply to a previous bot message
The @mention is automatically stripped from the message text before forwarding to Claude β so the agent sees a clean command without the @botname prefix.
Access control in groups
By default (mention-only), no pairing is required β anyone who mentions the bot will get a response. If you need per-user access control in groups, switch to allowlist policy and pair users as usual.
Media support
The server handles incoming photos and documents β not just text.
Photos:
- Downloaded from Telegram and saved to
/tmp/tg-photo-{file_unique_id}.jpg - Channel event text becomes:
[photo saved to /tmp/tg-photo-<id>.jpg Caption: "..."] - Agent reads the file via Claude Code's native
Readtool (supports images natively)
Documents (any file type):
- Downloaded to
/tmp/tg-doc-{file_unique_id}-{filename} - Channel event text becomes:
[document: filename.ext (mime/type) saved to /tmp/tg-doc-<id>-filename Caption: "..."] - Text files (
.txt,.md,.csv,.json,.yaml,.log) β agent reads withcat - Images (
.jpg,.png) sent as documents β agent reads withReadtool
Empty messages (photo without caption):
- Previously ignored by Claude Code (empty
contentin channel event) - Now: fallback text
[message received - no text content]ensures the message always reaches the agent
Example channel content for a photo with caption:
[photo saved to /tmp/tg-photo-AgACAgI.jpg Caption: "Here's the screenshot"]
Example channel content for a .md file:
[document: report.md (text/markdown) saved to /tmp/tg-doc-XYZ123-report.md Caption: "Review this"]
Your agent parses the path from the message text and reads the file directly.
Access control
By default, the server runs in allowlist mode β only paired users can send messages.
Pairing flow:
- Unknown user sends a message to the bot β bot replies with a 6-char code
- You tell Claude Code to pair that code β user is added to the allowlist
- Bot confirms in Telegram: "Bot authorized under name {botName}"
- User can now send messages that reach Claude Code
Pairing codes are single-use and per-user (sending multiple messages won't generate duplicate codes).
Open mode:
If you want anyone to message the bot:
Tell Claude: "set telegram access policy to open"
Permission relay
When Claude Code needs approval to run a tool (e.g., Bash), it forwards the request to Telegram:
π Permission request
Tool: Bash
Action: Run npm test
Reply "yes abcde" or "no abcde"
Approve or deny tool execution right from your phone β no need to sit at the terminal.
Debug mode
Debug mode enables verbose logging across all modules. Useful for troubleshooting bot connectivity, pairing issues, and channel communication.
Enabling
SSE mode: Set DEBUG=1 in the launchd plist's EnvironmentVariables or when starting the server:
DEBUG=1 PORT=3200 TRANSPORT=sse node dist/index.js
Stdio mode: Set DEBUG=1 in .mcp.json env block.
Where logs go
- SSE mode:
/tmp/ceo-agent-tools-channels.log(configurable viaMCP_LOG_FILE) - Stdio mode: stderr (captured by Claude Code)
Log prefixes
| Prefix | Module | What it logs |
|---|---|---|
[telegram-mcp] | index.ts | Startup, bot connection, polling, pairing codes, authorized messages |
[config:debug] | config.ts | Env vars, bot registry lookup, token resolution |
[telegram-api:debug] | telegram.ts | Every Telegram API call and errors |
[access:debug] | access.ts | Access file load/save, pair code lookup, allowlist changes |
[tools:debug] | tools.ts | Tool invocations with arguments, pair results |
[channel:debug] | channel.ts | Channel notifications emitted to Claude Code (full JSON payload) |
Example debug output
[telegram-mcp] Starting MCP server...
[config:debug] TELEGRAM_BOT_NAME="devops"
[config:debug] Token found in registry for "devops"
[telegram-mcp] Config loaded: bot="devops", accessList="~/.claude/telegram-access-devops.json", poll=1000ms
[telegram-mcp] Bot connected: @cc_devopsbot (CC devops bot)
[telegram-mcp] MCP server started on stdio
[telegram-mcp] Polling started
[telegram-mcp] Pairing code "abc123" generated for user 123456789 (chat 123456789)
[access:debug] pair("abc123"): success, allowedUsers=[123456789]
[telegram-mcp] Sending authorization confirmation to chat 123456789 for bot "devops"
[channel:debug] Emitting channel notification: {"method":"notifications/claude/channel",...}
Disabling
Set DEBUG=0 or remove the DEBUG env var. Non-debug logs ([telegram-mcp]) always print regardless.
Process cleanup
SSE mode: The server runs persistently via launchd. Individual sessions are cleaned up when the client disconnects. The server itself only stops on SIGINT/SIGTERM.
Stdio mode: The MCP server exits when Claude Code closes stdin or on SIGINT/SIGTERM/SIGHUP.
Troubleshooting
| Symptom | Fix |
|---|---|
/stop returns "No tmux session" | Run tmux ls and verify a session named after your bot exists. If you launch via a script outside tmux, wrap it: tmux new-session -s <botName> "cd ~/agents/<botName> && claude-tg". |
/stop fails with ENOENT on launchd | launchd PATH does not include Homebrew. Add /opt/homebrew/bin to EnvironmentVariables in your plist (see Setup, step 5). |
| Status message freezes on long subagent runs (β₯1 min) | Upgrade to v3.1.9 β COUNTER_RE substitution and watcher hash dedupe removed; timer text now flows live. |
Status updates go to an old message after /stop and a new task | Upgrade to v3.1.0 β StatusManager.findTaskByChatId now returns the most-recent active task instead of the oldest. |
| Status updates from bot A appear in bot B's chat (cross-bot leak) | Upgrade to v3.1.1 β findTaskByChatId now filters by botName in addition to chatId. |
| Agent can't connect | SSE server not running. Run curl http://127.0.0.1:3200/health. If down, restart. |
| Messages not arriving | Wrong bot name in .mcp.json or polling error. Check stderr logs for [botname] Polling error. |
| "typing..." indicator stuck | Will auto-stop after 2 min. Or restart server. With v3.1.0, /stop also calls stopTyping immediately. |
Skills
The skills/ directory contains reusable agent skill files.
spawn-agent
File: skills/spawn-agent/SKILL.md
A complete step-by-step skill for creating a new agent from scratch. Covers everything:
- Register bot token in
~/.claude/telegram-bots.json - Create
telegram-access-{name}.jsonwith direct allowlist (no pairing needed) - Set up agent directory:
logs/,state/,.claude/skills/ - Write
.claude/settings.jsonwithbypassPermissionsand all tool permissions - Generate a task-specific
CLAUDE.md - Create the MCP config at
/tmp/claude-tg-mcp.{name}.json - Start the tmux session
- Update the architecture doc in Obsidian
- Send a completion report to Telegram
Usage: copy the skill file into your agent's .claude/skills/ folder, or reference it when asking Claude Code to create a new agent:
Create a new agent called "hiring" β it should review incoming CVs, score them against our criteria, and reply with a structured verdict.
Claude will follow the skill and build everything automatically.
Architecture
src/
βββ index.ts # Entry point: SSE/stdio server + Telegram polling + typing indicator + session routing
βββ config.ts # Bot registry (~/.claude/telegram-bots.json) + env vars β typed config
βββ telegram.ts # Telegram Bot API client (zero deps, pure fetch) + sendChatAction
βββ access.ts # Allowlist, pairing codes, policy management (per-bot files)
βββ channel.ts # Emits MCP channel notifications to Claude Code
βββ permissions.ts # Permission relay (Claude Code β Telegram)
βββ tools.ts # MCP tool definitions and handlers
SSE mode internals
HTTP Server (port 3200)
βββ GET /sse?bot=NAME β SSE connection, creates MCP Server per session
βββ POST /messages?sessionId=xxx β MCP messages from clients
βββ GET /health β JSON status (sessions, bots, typing count)
Shared state:
βββ Telegram polling (one loop per unique token)
βββ Session map (sessionId β { server, transport, botName })
βββ Typing intervals (botName:chatId β setInterval)
βββ Token alias map (token β [botName1, botName2, ...])
Acknowledgements
Based on the official Anthropic Telegram Channel Plugin (telegram@claude-plugins-official). Rewritten from scratch in TypeScript/Node.js with multi-agent support as the primary design goal.
Requirements
- Node.js >= 18
- Claude Code with channels support (v2.1.80+)
- Claude.ai login (not API key)
License
Source Available β free to use, fork, and modify. Commercial use (selling the software or services based on it) is not permitted without a separate license. See LICENSE for details.
Copyright Roman Belopolskiy / 4sell.ai β for commercial licensing: r.belopolskiy@4sell.ai
