[BUG] Telegram plugin's MCP server hijacked by other Claude Code sessions, ignoring `--channels`
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 at user scope, every Claude Code session that loads the plugin claims the Telegram bot's long-poll lock — regardless of whether that session was launched with --channels plugin:telegram@....
Concretely:
- The plugin's MCP server (
server.ts) is a long-running bun subprocess started whenever the plugin loads. - On startup, it reads
~/.claude/channels/telegram/bot.pid, sends the previous PIDSIGTERM, writes its own PID, and startsbot.start()(long-poll). This is unconditional — there is no flag, env var, or check that gates it. - Telegram's Bot API permits exactly one
getUpdatesconsumer per token. The plugin's "newest wins" takeover means that any secondary Claude Code session —claude -pfrom a script, a cron job, an IDE invocation, a parallel interactive session, a subprocess spawned by user code — silently kills the listener session's bun. - The listener session sees a generic MCP disconnect with no cause given. Inbound Telegram messages stop arriving. Recovery requires manual
/mcpreconnect. - The
--channels plugin:telegram@...flag — which a reasonable user assumes is what designates "this is the listener" — has no effect on the plugin's takeover behavior. It only controls whether the parent Claude Code process delivers inbound messages into its prompt; the flag is never propagated to the bun subprocess.
Net effect: on a single-user machine running a persistent Hobbes/Telegram-listener session and any other Claude Code activity, Telegram inbound delivery breaks silently and repeatedly throughout the day. We hit this 4+ times in 11 hours.for me into
What Should Happen?
The intuitive contract of --channels plugin:X is "this session is the channel listener for plugin X." That should be enforced end-to-end:
- Only the session designated by
--channelsshould hold the long-poll lock. Other sessions with the plugin loaded should expose the plugin's tools (so they canreply,react,send_chat_actionprogrammatically) but must not claimbot.pidor callbot.start(). --channelsshould propagate to the plugin subprocess. Today the bun has no way to know whether its parent claude was launched as a listener or a tool-only consumer. The harness should pass that signal through (e.g. an env varTELEGRAM_CHANNEL_ACTIVE=1set when--channels plugin:telegram@...is in argv).- The plugin should default to tools-only. The takeover routine and
bot.start()should be gated on the env var above. Default = tools available, no polling. Listener mode = explicit opt-in via the env var the harness sets from--channels. - **When the listener is evicted (legitimately, e.g. user manually intervenes), the harness should surface a clear message** in the listener session, not a bare "MCP disconnected." Today the user has to read the plugin's per-launch log file in
~/.cache/claude-cli-nodejs/.../mcp-logs-plugin-telegram-telegram/to find out their listener was hijacked.
The combination of (1)-(3) is a small plugin patch plus a small harness change. The combination would make --channels mean what users already expect it to mean.
Error Messages/Logs
Steps to Reproduce
Environment: Ubuntu 24.04, Claude Code latest, telegram plugin v0.0.6.
- Install and enable the telegram plugin globally (user scope) with a valid bot token at
~/.claude/channels/telegram/.env. - Launch session A (the intended listener):
```
claude --channels plugin:telegram@claude-plugins-official
`
Confirm it can react/reply to Telegram messages. Note its bun PID:
``
pgrep -f "bun.*plugins/cache/.*telegram"
- From a separate terminal, launch session B without
--channelsin any other directory:
```
claude -p "echo hi" --model sonnet
--channels` flag, it loads the telegram plugin, spawns its own bun, and that bun calls the "replacing stale poller" routine that SIGTERMs session A's bun.
Even though session B has no
- Result: session A's MCP connection silently drops. Inbound Telegram messages no longer arrive in session A's prompt. From the user's perspective, "Telegram is broken" with no indication why.
- Worse: when session B exits (e.g.,
claude -pfinishes), its bun exits too. The poller slot is now empty — Telegram is fully dark until the user manually/mcpreconnects in session A or restarts the session.
This reproduces 100% of the time. Any secondary claude that loads the plugin — including ones spawned as subprocess by user code (we hit this with a Python wrapper that calls claude -p for analysis tasks), cron-spawned claude -p runs, IDE claude invocations, and --bare-less --print calls — will sever the listener session.
Claude Model
Not sure / Multiple models
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.123 (Claude Code)
Platform
Anthropic API
Operating System
Other Linux
Terminal/Shell
Other
Additional Information
_No response_
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗