[BUG] Telegram plugin auto-loads in all Claude Code sessions, not just --channels sessions
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When the Telegram plugin is installed, every claude -c session (without --channels flag) automatically spawns a Telegram bun process that polls for messages. This causes multiple consumers competing for the same Telegram bot token, resulting in dropped/lost messages.
Steps to reproduce:
- Install Telegram plugin:
claude plugin add telegram@claude-plugins-official - Start session A:
claude --channels plugin:telegram@claude-plugins-official - Start session B:
claude -c(no --channels flag) - Both sessions spawn separate Telegram bun processes polling the same bot token
- Messages are randomly delivered to either session — ~50% message loss on the intended session
What Should Happen?
Expected behavior:
Only sessions explicitly started with --channels plugin:telegram@claude-plugins-official should spawn the Telegram plugin process. Plain claude -c sessions should not load channel plugins.
Actual behavior:
All sessions load installed plugins regardless of --channels flag. Each spawns its own bun server.ts process for the Telegram plugin, creating competing consumers on the same bot token.
Workaround:
Manually kill the extra bun/server.ts processes spawned by non-channel sessions.
Impact: Critical for Telegram users — messages are silently lost with no error indication.
Error Messages/Logs
Steps to Reproduce
- Install Telegram plugin: /install telegram@claude-plugins-official
- Start terminal A: claude --channels plugin:telegram@claude-plugins-official
- Start terminal B: claude -c (no --channels flag)
- Run: ps aux | grep "server.ts" — shows TWO telegram bun processes
- Send a Telegram message — it randomly goes to terminal A or B
- ~50% of messages silently lost on the intended session
Claude Model
Opus
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.81 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
_No response_
23 Comments
same happen with me
I'm also experiencing this issue. After exiting Claude Code, the Telegram plugin's bun process is not terminated and keeps running until CPU hits 100%.
Environment:
The orphaned
bun server.tsprocess from the Telegram channel plugin persists after session exit and gradually consumes all available CPU resources.@liysx try to update to 2.1.83
@SG5 Still the same issue
The core issue persists on v2.1.83 — every new Claude Code session still spawns a Telegram plugin process regardless of whether --channels was passed.
Each bun process now terminates properly when its parent session exits. The number of running Telegram plugin processes matches the number of active sessions exactly.
The practical impact remains: multiple competing consumers on the same bot token cause messages to be randomly routed to unintended sessions, resulting in probabilistic message loss.
Confirmed + Root Cause + Reference Implementation
Reproducing this on macOS 15 (Darwin 25.3.0 arm64) with Claude Code v2.1.85 + VSCode extension. The VSCode sidebar panel also loads the Telegram plugin even without
--channels, causing the same competing-consumers problem.Root cause (from source inspection)
From
cli.js, the plugin loading path (Ol_()) readsenabledPluginsfrom~/.claude/settings.jsonat startup and unconditionally starts the MCP server for every matching plugin — regardless of whether--channelswas passed. The--channelsflag is a separate subsystem: it only registers an already-running MCP server for inbound channel notifications. So:enabledPlugins: true→ every CC instance starts the bot (polling begins)--channels→ marks that instance as the one that should handle inbound pushesBoth are required for the channel to work, but the decoupling means every instance starts polling, even though only one should.
Workaround (better than killing processes)
Add
"telegram@claude-plugins-official": falseto a project-level.claude/settings.local.jsonin the working directory used by your non-channel instances (e.g. your VSCode project root). This overrides the globalenabledPluginsfor that instance only.Reference implementation: OpenClaw
OpenClaw (
github.com/openclaw/openclaw) — an open-source AI messaging gateway that also runs Telegram bots — handles this exact problem cleanly. Their approach (extensions/telegram/src/):update-offset-store.ts): stores{ lastUpdateId, botId }to disk, validatesbotIdon load to reject stale offsets from a different tokenbot.ts): tracks which update IDs are currently being processed; only advances the "safe watermark" below the smallest in-flight ID — prevents skipping updates under concurrent processingcreateTelegramUpdateDedupe()): secondary guard within a sessionResult: multiple instances can safely share the same bot token. Each receives every update, but deduplication ensures each is processed exactly once — no message loss.
This is the pattern I'd suggest as a reference for fixing the plugin-level conflict here.
I will update to the new version and trial again
Simpler Global Workaround + Architectural Proposal
So I am running a Telegram bot via --channels in a dedicated tmux session. Meanwhile, Claude Desktop's Code tab had a week-old session that was never configured with --channels. That session started receiving and responding to my Telegram messages using its own unrelated context. Messages were routed to the wrong session with zero indication.
Evidence:
of enabledPlugins
Current workaround I found with claude:
The existing workaround from @rzblues (per-project settings.local.json) requires adding an override to every project directory. Misses any new project. Which I will add, @rzblues suggestion did help me a ton with figuring this out as well.
Our approach — one global setting:
// ~/.claude/settings.json
{
"enabledPlugins": {
"telegram@claude-plugins-official": false
}
}
Then launch the dedicated channel session with:
claude --channels plugin:telegram@claude-plugins-official
Why this works: --channels loads and activates the plugin independently of enabledPlugins. Setting
enabledPlugins: false globally prevents every other session (including Claude Desktop Code tab,
VSCode sidebar, subagents, scheduled tasks) from loading the plugin. Only the session with
--channels polls Telegram.
Verified:
Important: enabledPlugins: {} (empty object) is NOT the same as enabledPlugins: { "telegram@...":
false }. Empty means "use defaults" — Desktop still loads installed plugins. Explicit false is
required.
Also important: claude plugin install resets enabledPlugins back to true. After any plugin
reinstall/update, you must manually set it back to false.
Additional Issues We Hit
it, the trust prompt blocks headless restarts and the watchdog crash-loops.
other apps" on every restart. Fix: add tmux + terminal app to System Settings → Privacy & Security →
Full Disk Access.
"Claude API" (Sonnet 4.6) instead of "Claude Max" (Opus 4.6). Removing the CLAUDE_CODE_OAUTH_TOKEN
env var and using normal login auth restores Max plan access.
### _Architectural Proposal: Named Session Channel Routing_
The root issue is that channel plugins have no concept of "which session should I serve." The
current architecture:
enabledPlugins: true → every session starts polling
--channels → marks one session for notifications (but polling already started
everywhere)
Proposed: session-scoped channel assignment.
// ~/.claude/settings.json
{
"channelSessions": {
"telegram-bot": {
"channel": "plugin:telegram@claude-plugins-official",
"autostart": true
},
"discord-bot": {
"channel": "plugin:discord@claude-plugins-official",
"autostart": true
},
"imessage-bot": {
"channel": "plugin:imessage@claude-plugins-official",
"autostart": false
}
}
}
Or at minimum, --channels should be the ONLY mechanism that starts the plugin's MCP server polling.
If a session doesn't have --channels, the plugin should not start — even if it's in enabledPlugins
or installed_plugins.json. The enabledPlugins flag should control tool availability only, not
polling process startup.
This would also enable multi-channel architectures where one machine runs multiple named sessions —
one for Telegram, one for Discord, one for iMessage — each isolated, each with its own context.
Environment
Workaround seems to be working fine for the last 2 days.
telegram@claude-plugins-officialfrom global~/.claude/settings.json→enabledPlugins.claude/settings.jsonin the specific workspace where I want Telegram, with{"enabledPlugins": {"telegram@claude-plugins-official": true}}.This way, only sessions running in that project directory start the Telegram MCP server. Other Claude sessions in different projects don't spawn zombie bun processes.
Will close this if stable after 7 days.
Please don't close this as this is just a workaround. I would like the Claude code telegram to run on my root folder
@rzblues okay, will keep it open
Since I run multiple sessions in the same project, the
enabledPluginsapproach doesn't work for me — all sessions inherit the same setting. Took a different approach with aSessionStarthook that cleans up at the process level instead: https://github.com/patrick-yip/claude-telegram-guardOn every session start, it finds all bun processes running the Telegram plugin, kills any whose parent Claude process doesn't have
--channels, and keeps only the newest if multiple legit sessions exist. Tested on macOS.Suggest keeping this open until Anthropic fixes plugin loading in non-channel sessions at the source.
Workaround: use
--dangerously-load-development-channels server:<name>instead of--channels plugin:<name>@<marketplace>We've confirmed that the
--channels plugin:telegram@claude-plugins-officialpath does not deliver inboundnotifications/claude/channelto the session, while a bare.mcp.jsonserver entry with--dangerously-load-development-channelsworks end-to-end on the same machine, same token, same Claude Code version (2.1.92).Steps
.mcp.json(or~/.claude.jsonfor global):(Adjust the version path to match your cached plugin version.)
--channels:<channel source="telegram" ...>tag and Claude responds via thereplytool.Why this works
The
--dangerously-load-development-channels server:<name>flag activates channel routing for a bare MCP server entry (keyed by the name in.mcp.json). It bypasses the plugin resolution + allowlist path that--channels plugin:<name>@<marketplace>uses. The MCP server itself is identical — same binary, same capability negotiation — but the CLI-side activation path is different and apparently not affected by the bug.Environment
This also works for Discord and custom channel servers — any plugin that advertises
experimental.claude/channelcan be registered as a bare.mcp.jsonentry and activated with--dangerously-load-development-channels server:<name>.Update: We've published a full Rust drop-in replacement for the official Bun-based Telegram plugin: hdcd-telegram (v0.1.0).
access.jsonand.env— zero migrationWorks with the
--dangerously-load-development-channels server:telegramworkaround described above. Pre-built binaries for Linux, macOS (Intel + Apple Silicon), and Windows available on the releases page.Hi - we've put in some fixes to channels that address zombie
bun server.tspollers / 409 Conflict. Please update your channel version for telegram to 0.0.6.---
_Generated by Claude Code_
Seeing the same class of issue on a newer Claude Code build. Reporting my environment in case it helps narrow things down:
2.1.108 (Claude Code)telegram@claude-plugins-officialv0.0.6.claude/settings.json:``
json
``{
"enabledPlugins": {
"telegram@claude-plugins-official": true
}
}
When launching with the documented flag:
Claude Code prints:
So on
2.1.108the--channelsflag appears to be rejected outright (Channels are not currently available), which makes the original workaround in this thread ("only start the telegram consumer when--channelsis passed") impossible to apply — any session with the plugin enabled at user/project level still auto-spawns thebun server.tsconsumer, and there's no supported way to opt in per session.Happy to provide additional logs /
psoutput if useful.Specific sub-case: Agent Team (TeamCreate) spawns competing Telegram instances
We're hitting this with the Agent Team feature. The reproduction path:
TeamCreateto create a team, then spawn teammates viaAgentwithteam_nameparameter--channels plugin:telegram@claude-plugins-officialflagbun server.tsfor the Telegram pluginbunprocesses now compete forgetUpdateson the same bot token → 409 Conflict loops, missed messages, duplicated deliveriesThis is a specific and reliable reproduction case that may not be obvious from the general "multiple instances" framing — the user only starts one Claude Code session, but the team infrastructure multiplies it.
Current workaround: A
SubagentStarthook that detects and kills competingbun server.tsinstances before a new teammate starts polling. This is fragile — it races with the teammate's own startup, and killing a process that's mid-reply can drop outgoing messages.Better fix: The PID lock approach from #39876 would solve this cleanly. Only the first teammate's Telegram process polls; all others serve tools only (outgoing replies via
bot.api.sendMessagestill work without polling). We've applied this locally and it works well with 3–5 teammates sharing a single bot token.Confirmed reproduction on Windows 10 (Claude Code 2.1.101, VS Code extension + CLI).
Mechanism, pinned to specific PIDs from a single run:
claude --channels plugin:telegram@...spawns bun server.ts PID 132572. Writes its PID tobot.pid. Polls getUpdates successfully, Telegram bridge works.--channelsflag). The extension loads the Telegram plugin anyway. Its bun server.ts (PID 4672) starts, readsbot.pid, sees the alive incumbent, and issues SIGTERM against it (server.ts lines 59-69 in v0.0.6). PID 132572 dies.bot.pid. Now nothing is running, and the original--channelssession still has no Telegram bridge.So the user-facing symptom ("Telegram disconnects randomly after a couple of hours") is actually: any other Claude Code session loading the plugin kills the legitimate poller.
Local mitigation (v0.0.6): inserted a startup guard in
server.tsbefore the PID-file logic that walks the process ancestry (WMI on Windows,/procon Linux,pson macOS), finds the nearestclaude/claude.exeancestor, and checks its command line for--channels. If absent, respond to MCPinitializewith emptycapabilitiesandexit(0), never touchingbot.pid.--channelssessions run the existing code path. Fails open on ancestry-walk errors.Result in a controlled reproduction: opening a VS Code session spawned 4 launcher + 4 server.ts buns (multiple VS Code sessions were open). All 4 server.ts processes yielded and exited within ~10 seconds. The
--channelssession's bun survived untouched and continued receiving Telegram messages. End-to-end tested.Happy to share the diff if useful. The cleaner upstream fix discussed in #36960 (plugin singleton mode) would obsolete this.
+1, confirming on Claude Code 2.1.117 (macOS 26.3.1).
Symptom we were chasing: in session A (started with
--channels plugin:telegram), the telegram MCP shows "disconnected" in a<system-reminder>and/mcpreports the server as gone. Every time we reconnect, it stays up for a while then drops again.Trigger: starting or reconnecting in session B. Session B was deliberately started without
--channels, which (per the original assumption) should not load the plugin — but per this bug it does, silently. So session B auto-spawns its ownbun server.ts, callsgetUpdateson the same bot token, and Telegram's 409-Conflict behavior kicks whichever long-poller was holding the lock. Last session to start (or to run/mcp) wins, the other's plugin exits.That explains the ghost symptoms we saw:
ps aux | grep server.tsoften shows only one telegram process — the loser already exited by the time you check./mcpreconnect in session A, stderr logstelegram channel: replacing stale poller pid=<old>— confirming the previous long-poller is being displaced.Only workaround needed: if session A is the one you want reachable via Telegram, just
/mcpin session A after session B has started — last-writer-wins means A takes the lock back and stays up until session B does the same. Fragile but self-healing without anypkill.Real fix is what the OP suggests — plugins declared under
--channelsshould only load in sessions started with--channels.I was troubled by this issue for a long time until I recently discovered that the problem wasn’t where I had thought it was.
By patching the server.ts file in the Telegram plugin, I have now completely resolved the issue. I can now use the latest version of the Telegram plugin without any disconnections from MCP.
Related issue: https://github.com/anthropics/claude-plugins-official/issues/1481
Closed PR: https://github.com/anthropics/claude-plugins-official/pull/1461
Adding a fresh data point on a newer build — bug persists in Claude Code 2.1.138 / Telegram plugin 0.0.6, and I hit a stronger failure mode than what's been reported above.
What I saw
Six
bun server.tsprocesses from the telegram plugin had accumulated across sessions, each pegged at 100% CPU. Oldest was running since April 21 (~3 weeks of wall time, ~95 hours of CPU time). They survived parent-Claude-Code deaths and ignoredSIGTERM— I had toSIGKILLall six.So this is two failure modes layered on top of each other:
Code observations
The plugin's
server.ts(v0.0.6) has three independent mechanisms intended to prevent exactly this — all three failed in my case:SIGTERMto any prior PID holder. In my case the prior holders ignored SIGTERM, so new sessions couldn't displace them. The guard never escalates to SIGKILL.process.ppid !== bootPpidevery 5s. Also didn't catch the reparent, for reasons I couldn't determine without more instrumentation.The retry loop (lines 993-1032) has bounded backoff (max 15s), so the 100% CPU isn't from that loop spinning — my guess is grammy's
getUpdatesended up in a state where its internal HTTP retry pattern was busy-looping (network hiccup + a specific error type?), and none of the three shutdown paths could fire to stop it.Mitigation I'm running locally
For folks who genuinely need telegram in multiple concurrent sessions (so @nickcarmonadigital's
enabledPlugins: false+--channelsglobal workaround doesn't fit), a complementary defense is aSessionStarthook that kills any telegram bridge with sustained high CPU —>50% AND age >5min. Healthy bridges idle near 0% CPU, so it spares concurrent sessions and only catches actual zombies. Happy to share the script if useful.Suggestions for a real fix
--channelssession should callbot.start().Filed #66309 independently: Diagnosed the root cause as the orphan watchdog checking process.ppid !== bootPpid, but missing the case where bun run (the direct parent) gets reparented to init when CC exits. server.ts's own ppid never changes, so the watchdog doesn't fire. Fix: read grandparent PID from /proc/<ppid>/status at startup and check for reparenting. Patch available in #66309.
Another data point + a workaround not yet in this thread (token isolation)
Confirming the bug persists on Claude Code 2.1.195 / Telegram plugin 0.0.6 (macOS). On this build
--channels plugin:telegram@claude-plugins-officialdoes work for the dedicated session (contrary to the 2.1.108 "Channels are not currently available" report above), but the underlying problem is unchanged: every otherclaudeprocess on the machine still auto-spawns its ownbun server.tspoller.Trigger worth adding to the list: a scheduled, always-on background agent on the same machine. I run a dedicated channel-host session (a supervisor that launches
--channels) alongside a separate 24/7 agent that alaunchdjob relaunches every ~30–45 min. Each relaunch spawned a competing poller, and the plugin's stale-poller cleanup (server.tsreadsbot.pidandSIGTERMs the incumbent) killed the host session's listener like clockwork — the competing spawn timestamps matched the listener deaths to the second. Same mechanism @CalebC83 and @bryanhong describe (replacing stale poller pid=), but driven by a scheduler rather than a human opening VS Code, so it recurs ~hourly, around the clock. A watchdog kept auto-restarting the host, which turned into ~20 "recovered" notifications overnight.Workaround — token isolation (no per-project config, no plugin patching, no hooks):
server.tsreads the bot token andprocess.exit(1)s if it's missing — and that check runs before the PID-file stale-poller-kill. So if non-host sessions can't see the token, their pollers exit cleanly at startup and never touchbot.pid.TELEGRAM_BOT_TOKENfrom the shared~/.claude/channels/telegram/.env.--channelssession exportsTELEGRAM_BOT_TOKEN(sourced from a private chmod-600 file) before launchingclaude. The plugin's "real env wins over .env" logic picks it up; confirmed in the spawned poller's environment viaps eww.Result: the host session polls; every other session (VS Code, subagents, scheduled/background agents, plain
claude) logstelegram channel: TELEGRAM_BOT_TOKEN requiredand exits immediately, leaving the host untouched.Why I'd reach for this over the other workarounds in the thread:
.claude/settings.local.jsonto maintain across directories (and it sidesteps thesettings.local.jsonmerge quirk).claude plugin install/update, which resetsenabledPluginsback totrue.--channels/enabledPluginsbehavior at all, so it still holds on builds where those are rejected or unreliable.Verified end-to-end: the host survives a forced relaunch on the env-only token; a real competing
claude -pin another directory loggedTELEGRAM_BOT_TOKEN requiredand exited while the host's poller PID stayed put; inbound messages keep arriving.Agreed with the thread that the real fix is decoupling load-from-poll — only sessions started with
--channelsshould callbot.start().---
_Drafted with Claude Code._