Chat with your Claude Code sessions from Telegram — with many bots, and full control over which session each bot talks to.
This is a fork of Anthropic's official telegram plugin for Claude Code, which does a wonderful job of bridging a Telegram bot to a session, with pairing, allowlists, and group support built in. Telegram Switch keeps all of that and adds two things on top: explicit per-session connect/disconnect, and multiple bots you can pick between.
/tg:connect # pick a bot → this session now receives its Telegram messages
/tg:status # see every bot and which session holds it
/tg:disconnect # release
Each bot is claimed independently, so you can run one bot per project, one per client, or one for alerts and one for conversation — and a single session can hold several at once. Run /tg:connect for the same bot in another session and it moves there: the previous session gets a polite note in its transcript, and nothing is lost — while no session holds a bot, Telegram queues its messages (for about 24 hours) and delivers them on the next connect.
Sending is never restricted: any session can use the reply, react, and edit_message tools at any time. Only receiving is exclusive.
- Claude Code (CLI or desktop) with plugin support
- Bun — the plugin's MCP server runs on it
- A Telegram bot token from @BotFather (
/newbot, takes a minute)
claude plugin marketplace add xlitedev/claude-telegram-switch
claude plugin install tg@telegram-switchIf you currently use the official telegram plugin, disable it — both plugins would otherwise poll the same bot token, and Telegram allows only one consumer per token:
claude plugin disable telegram@claude-plugins-officialYour existing state in ~/.claude/channels/telegram/ (token, pairings, allowlists) carries over: the first time the new server boots it moves it into bots/default/, so your current bot keeps working under the id default. Your old .env is left in place as a backup.
Inbound channel messages ride on a Claude Code feature that is still gated, so sessions must be launched with the flag naming this plugin:
claude --dangerously-load-development-channels plugin:tg@telegram-switchOutbound tools and all the slash commands work without it — only receiving Telegram messages needs it. Worth putting in a shell alias.
Restart your Claude Code sessions after installing.
-
Create a bot: message @BotFather, send
/newbot, follow the prompts, and copy the token (looks like123456789:AAH...). -
Register it under a short id of your choosing — in any Claude Code session:
/tg:configure add alerts 123456789:AAH...(This writes
~/.claude/channels/telegram/bots/alerts/.env. You can create that file yourself withTELEGRAM_BOT_TOKEN=...inside.) -
Connect and pair:
/tg:connectYou'll get a picker listing your bots, each with its
@usernameand whether it's free or already held by another session. Choose one, then DM that bot anything on Telegram — it replies with a 6-character pairing code. Back in Claude Code:/tg:access alerts pair <code>Done — your DMs now reach the connected session, and Claude's replies come back to your Telegram chat.
Repeat steps 1–3 for as many bots as you like. Pairing is per-person per bot, not per-session: once paired you can switch a bot between sessions freely, and each bot keeps its own allowlist so access never leaks between them. /tg:access also manages group chats and DM policy — see ACCESS.md for the full model.
A project can pin which bots it uses, so /tg:connect offers the right ones first and defaults to the right one. Create <project>/.claude/telegram.json:
{
"bots": ["alerts", "support"],
"default": "alerts"
}Or let the skill write it for you, from the project directory:
/tg:configure project alerts support
Ids only — never tokens. This file is safe to commit; tokens stay in ~/.claude/channels/telegram/bots/<id>/.env on each machine. For a bot you'd rather not commit, use .claude/telegram.local.json (same shape) and gitignore it. Both files are merged, with the local one winning on default, and parent directories are searched too — so a config at the repo root applies in every subdirectory.
Bots not listed by the project stay available; they're just shown as global rather than project in the picker.
| Command | What it does |
|---|---|
/tg:connect |
Pick a bot and claim it for this session. Inbound messages arrive as <channel> blocks; Claude answers via the reply tool. |
/tg:connect alerts |
Connect a specific bot, skipping the picker. |
/tg:connect alerts my-label |
Same, with a custom session label shown in /tg:status and in the Telegram ping (defaults to the project folder name). |
/tg:disconnect |
Release every bot this session holds. /tg:disconnect alerts releases just one. |
/tg:status |
Show every bot: @username, scope, who holds it, whether polling is live, any errors. |
/tg:access |
Manage pairing, allowlists, groups, DM policy — per bot. |
/tg:configure |
Add/remove bots, pin a project's bots, review setup. |
When you connect a bot, everyone on its allowlist gets a short Telegram ping naming the session it's now wired to:
🟢 Connected to a Claude Code session
bot: @MyAlertsBot (alerts)
session: arturcecan-md / main
dir: ~/workplace/clients/arturcecan
Messages you send here now reach that session.
Typical multi-session day:
# terminal 1 — project A
/tg:connect → picker → "connected — bot 'alerts' now delivers to this session"
# ... later, terminal 2 — project B needs your attention from the road
/tg:connect → picker shows: alerts | @MyAlertsBot | global | held by "project-a"
→ pick it anyway; it moves here, terminal 1 gets a transcript note
# holding two bots at once
/tg:connect support → "keep both connected, or only the new one?" → keep both
→ inbound blocks now carry bot="alerts" or bot="support"
While connected, everything the official plugin supports works as usual: text, photos, documents, voice notes, reactions, message editing, and Claude Code permission prompts relayed to Telegram as Allow/Deny buttons.
Telegram's Bot API allows exactly one getUpdates long-poll consumer per bot token. Each Claude Code session runs its own instance of this plugin's MCP server, so something has to decide which instance polls which bot.
Telegram Switch makes that decision explicit with a small claim file per bot, ~/.claude/channels/telegram/bots/<id>/owner.json, recording the owning process, a human-readable label, the project directory, and a timestamp:
- Every server instance boots idle — outbound tools work, no polling.
/tg:connectwrites the claim for the chosen bot and starts polling it in that session.- A lightweight watcher (every 2 s) in every instance enforces every claim it holds: if another session takes a bot, the current holder stops polling it within seconds and posts a short "bot moved" note to its own transcript; the new holder's poller then acquires the token (Telegram briefly returns 409 Conflict during the handoff — the plugin retries with backoff until it wins).
/tg:disconnectclears the claim; a claim held by a crashed process is flagged as stale in/tg:statusand can be taken over or force-released.
Because the claim is per bot, one session can hold several and another session can hold the rest — the 409 handoff only ever involves the one token being moved.
Each bot's state lives entirely in its own directory:
~/.claude/channels/telegram/
.env channel-wide settings (e.g. TELEGRAM_ACCESS_MODE)
bots/
alerts/
.env TELEGRAM_BOT_TOKEN=...
access.json who may reach this bot
owner.json which session currently holds it
bot-meta.json cached @username
approved/ inbox/
Everything else — pairing, access control, message handling, permission relay — is unchanged from the upstream plugin.
/tg:connect reports a lingering 409 Conflict. Some other process is polling that bot token — usually a session still running the official telegram plugin (disable it and restart that session), or an orphaned poller from a crashed session (ps aux | grep server.ts, then kill it). The connected session retries automatically once the token frees up.
/tg:status shows a claim held by a dead process. Harmless — run /tg:connect for that bot to take over, or /tg:disconnect <bot> (it will offer force) to clear it.
/tg:status says polling LIVE, but messages never arrive. The session was launched without the development-channels flag, so Claude Code drops the inbound injection before it reaches the transcript (the plugin's MCP log will show not in --channels list for this session). Relaunch as shown in Enable inbound messages. Note the messages consumed during such a window are acked to Telegram and won't redeliver.
Bot doesn't answer on Telegram. Check /tg:status in the session you believe holds it. If no session holds that bot, its messages are queued, not lost — connect it anywhere to receive them.
Claude replies from the wrong bot, or asks which bot to use. When a session holds more than one bot, the reply tool needs the bot id — it's in the bot attribute of the inbound <channel> block. Passing it is automatic in normal use; if you see the question, the message it's answering came from further back in the conversation.
A bot shows as unconfigured. Its directory exists but has no token — often a bot pinned by .claude/telegram.json that this machine hasn't set up. Run /tg:configure add <id> <token>.
Token errors (401/404). The token in ~/.claude/channels/telegram/bots/<id>/.env is wrong or was revoked — get a fresh one from @BotFather and run /tg:configure add <id> <token> again. A bot already loaded by a running session keeps the old token until that session restarts.
The repo doubles as a Claude Code plugin marketplace (.claude-plugin/marketplace.json), with the plugin itself in tg/. To hack on it locally:
git clone https://github.com/xlitedev/claude-telegram-switch
claude plugin marketplace add ./claude-telegram-switch
claude plugin install tg@telegram-switchAfter changing tg/server.ts or the skills, run claude plugin update tg@telegram-switch and restart your sessions (installs are cached under ~/.claude/plugins/cache/).
The server is a single TypeScript file run directly by Bun:
cd tg
bun install
bun run typecheck # tsc --noEmit, strict
bun run test # end-to-end suite against a mock Telegram APIbun run test spawns real server processes against test/mock-telegram.ts with TELEGRAM_STATE_DIR and TELEGRAM_API_ROOT pointed at throwaway fixtures, so it never touches your real bots or ~/.claude state. It covers the picker, project defaults, the connect ping, inbound bot attribution, holding several bots at once, release_others, cross-session takeover, and disconnect.
The same two env vars are handy for poking at it by hand: TELEGRAM_STATE_DIR=/tmp/tg-test bun tg/server.ts.
Built on Anthropic's official telegram plugin for Claude Code (forked at v0.0.6) — the pairing flow, access control, media handling, and permission relay are its work, and this plugin adds the session-switching and multi-bot layers.
Licensed under Apache 2.0, same as upstream.