Channel messages don't wake idle sessions (--channels plugin)
Description
When using --channels plugin:telegram@claude-plugins-official, incoming messages display in the terminal (as ← telegram · user: message) but do not trigger Claude to process them when the session is idle at the prompt. The REPL waits for keyboard input instead of interrupting to handle the MCP channel notification.
Steps to Reproduce
- Start Claude Code with
--channels plugin:telegram@claude-plugins-official - Let the session complete a task and return to the idle prompt (
❯) - Send a message to the bot via Telegram
- The message appears in the terminal as
← telegram · user: ... - Claude does NOT automatically process it — just sits at the prompt
Expected Behavior
Incoming channel messages should interrupt the idle prompt and trigger Claude to process/respond, similar to how they work during active conversations.
Actual Behavior
Messages arrive and display but the REPL stays at the idle prompt. The session only processes the message if the user manually types something (any input wakes it up).
Investigation
The Telegram plugin correctly sends notifications via mcp.notification({ method: 'notifications/claude/channel', ... }) and declares the claude/channel capability. The issue is that the REPL/harness does not actively subscribe to or process MCP channel notifications when idle — it prioritizes stdin over MCP notifications.
Workaround
A background script that monitors tmux for incoming ← telegram lines and sends keystrokes to wake the session. This works but is fragile.
Environment
- Claude Code v2.1.92
- macOS 26.3.1
- Telegram plugin v0.0.4
- Multiple concurrent sessions via tmux, each with their own bot token
🤖 Generated with Claude Code
11 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Not a duplicate — this issue has specific reproduction details and investigation findings (MCP notification delivery works fine, the issue is the REPL not subscribing to channel notifications while idle at the prompt). The duplicates (#37139, #36477, #38259) describe the same symptom but don't include the root cause analysis. Keeping open for visibility.
Telegram channel plugin is broken -- 65+ open issues, no fix in sight
The Telegram channel plugin was the main reason I subscribed to the Max plan (20x tier). It's never worked reliably. I spend more time restarting it and killing zombie processes than actually using it.
What actually happens
The bun process running server.ts dies. No error, no warning, nothing. The
claude --channelsparent just keeps running with nothing feeding it messages.Orphaned bun processes from previous sessions hang around with
.orphaned_atmarkers, eating 120+ MB each. My workflow is: start session, send a message, notice it's dead,ps aux | grep telegram, kill stale PIDs, restart. Repeat.Environment
This isn't a niche problem
I went through the issue tracker. There are 65+ open issues about the Telegram plugin. Same bugs, reported over and over, across every platform:
Inbound messages never reach the session (outbound works fine):
#36411 #36429 #36771 #36895 #37189 #37229 #37301 #37498 #38477 #38508 #38988 #39776 #40800 #41275 #42138 #43049 #43088 #43627 #43653 #45189 #45548 #46016 #46125 #46356 #46744
MCP server disconnects/crashes:
#36427 #36615 #36644 #36834 #36964 #45852 #45985 #46502
Stops processing after idle/activity:
#36477 #36988 #38259 #44380 #45408 #45521
Zombie processes and 409 conflicts:
#36800 #37624 #38204 #39876 #45590 #45651 #46504 #46637
Spawn/config failures:
#36841 #38098 #38600 #42221 #42641 #43939 #46334 #46617
General MCP stability (all channel plugins):
#33947 #36786 #37259 #39486 #40207 #43177 #43895 #45146 #45880 #46004 #46203
On top of that, there are ~100 closed Telegram issues -- 74 closed as duplicates, and core bugs like #40525 and #40440 closed as NOT_PLANNED with no explanation. Zero open PRs with fixes.
What needs to happen
The MCP connection between Claude Code and channel plugins needs to not drop. That's the fix. The bun subprocess dies or the stdio pipe breaks, and the session doesn't notice or recover. Whatever is causing the child process to crash needs to be found and fixed, and the connection needs to be resilient to transient failures.
In the meantime, auto-reconnect, orphan cleanup, and a health indicator would at least make it usable while the root cause is tracked down. But those are workarounds, not the fix.
Most importantly: consolidate these 65+ issues into one tracking issue, assign an owner, and communicate a timeline.
This isn't solved by Remote Control
Remote Control requires the Claude app. Channels meet you where you already are -- Telegram, Discord, group chats, bots, webhooks. They integrate into existing workflows in ways a dedicated app never will. And Remote Control already has auto-reconnect when the network drops. Channels need the same resilience.
Credit where it's due
Being able to message Claude Code from your phone while it works on your local codebase is genuinely transformational. The team has done incredible work on Claude Code overall, and this feature -- when it works -- is a glimpse of what the future of development looks like. The fact that 165+ issues have been filed about it isn't just frustration, it's demand. People want this to work because they can see how powerful it is.
But right now it doesn't work, and there's no visible progress toward fixing it. This feature was publicly announced, has official documentation, and is listed as a key capability of Claude Code plugins. It's part of what people are paying for on the Max plan. It deserves the same level of engineering attention as the rest of the product.
cc @bcherny @ThariqS @felixrieseberg @chrislloyd @dicksontsai @ashwin-ant @natemcmaster @domdomegg
Field report from two Claude Code orchestrators running \
--channels plugin:discord@claude-plugins-official\. Confirmed same behavior with the Discord plugin — idle sessions don't wake on inbound channel messages. The tmux-keystroke workaround works but as noted, fragile.An alternative that we've been running locally in our patched \
server.ts\: after every successful \mcp.notification({ method: 'notifications/claude/channel', ... })\call in \handleInbound\, write the new message's ID to a nudge file:\
\\ts\// At end of handleInbound(), right after the mcp.notification().catch():
const nudgeDir = process.env.DISCORD_STATE_DIR ?? join(homedir(), '.claude', 'channels', 'discord')
const nudgePath = join(nudgeDir, '.nudge')
try { writeFileSync(nudgePath, msg.id) } catch {}
\
\Then an external watcher (e.g. \
inotifywait -m -e modify .nudge\+ \tmux send-keys\) can detect the file change and wake the idle session with a single Enter keystroke. The nudge-file approach is more reliable than \tmux capture-pane\output scraping because:fs.writeFileSync\per inbound message, no pollingRunning this in production since 2026-04-13 across two Claude Code boxes on Ubuntu 24.04. Same idea applies to any \
--channels plugin:X\that uses the same notification path — the nudge-file write lives in the plugin server's inbound handler, not in the REPL/harness. This is a plugin-side workaround until the REPL/harness gets proper MCP-notification-driven wakeup.Happy to submit as a follow-up PR on the discord plugin side (\
anthropics/claude-plugins-official#1153\) since that's where the change lives. The proper long-term fix is still in the Claude Code REPL/harness — just flagging an interim workaround that's less fragile than tmux scraping.Adding our findings from #48404 (will close as duplicate):
Environment: Windows 10, Telegram channel plugin v0.0.4
Additional evidence — messages are dropped, not queued:
This confirms: MCP channel notifications during idle are silently discarded, not buffered. The plugin layer works correctly (sends typing indicator + MCP notification), but the session layer drops the notification.
Also tried Windows Task Scheduler sending F15 keypresses every 60s — doesn't help because keystrokes don't reach the REPL stdin.
This is a significant blocker for remote/async workflows (e.g., founder traveling, managing operations via TG).
Same issue here. Adding evidence that isolates the bug to the Claude Code client's MCP notification handler (not plugin, not runtime, not token, not bot):
Setup:
Feature flags confirmed ON (
~/.claude.json):What works end-to-end:
getMereturns OK (token valid)getUpdatesreturns[]= consuming messagesgate()approves senderId in allowlisthandleInbound()runs without error (confirmed via stderr tee to file)mcp.notification({ method: 'notifications/claude/channel', params: {...} })resolves without rejectionlsof)What fails:
Tests I ran today to isolate the cause (2026-04-16):
| # | Test | Result |
|---|------|--------|
| 1 | Full wipe
rm -rf node_modules bun.lock bot.pid+ freshbun install| Bug persists || 2 | Patch
process.stdin.resume()at top ofserver.ts(ref #38736) to keep bun event loop alive | Bug persists || 3 | Replace the plugin entirely with hdcd-telegram (Rust binary, completely different runtime) and invoke via
claude --dangerously-load-development-channels server:hdcd| Bug persists — identically || 4 | Same bot token polled by a separate external daemon (bypassing Claude Code) | Works fine — confirms API + token + network are healthy |
Conclusion: Test #3 is the smoking gun. A completely different implementation (Rust vs Bun, different MCP SDK binding, different process topology) produces the exact same failure. This is not the plugin. This is Claude Code's client-side handler for
notifications/claude/channelsilently dropping inbound messages.Changelog reference: v2.1.105 noted "Fixed inbound channel notifications being silently dropped after the first message for Team/Enterprise users." I'm Team, on v2.1.111 (6 releases later), and not even the first message passes. Either the fix was incomplete, or a regression was introduced in v2.1.106+.
Cross-referenced: #36411, #36431, #36472, #36802, #37301, #37633, #37933, #38534, #38736, #40729, #41733, #46299, #48785 — zero maintainer response across all.
Any triage ETA? The feature is currently unusable on Claude Team.
Working workaround found — ~5–9s response latency, currently in testing
After weeks of debugging I found a fully working solution. The root cause is confirmed: Claude Code receives the MCP notification on the stdio transport but silently drops it internally. The plugin side is fine.
The fix uses a file-based inbox + shell watcher as an alternative delivery path.
1. Patch
server.ts— add this block insidehandleInbound, immediately before themcp.notification(call:2. Add
inbox_watcherto your launch script — polls every 5s, fires only when Claude is idle, auto-restarts on stall:3. (Optional) Persistent typing indicator — add to
server.tsto keep "typing..." visible for the full response duration. See full blueprint for thestartTypingLoop()snippet.Critical pitfalls:
tmux new-sessionsilently fails in headless/systemd withoutexport TERM="xterm-256color"and-x 220 -y 50kill 0in the trap → SIGSEGV ~60s after startup — usekill $WATCHER_PIDinstead"esc to interrupt"— otherwise watcher spams while Claude is processingFull blueprint with systemd setup, cron fallback, verification checklist and all pitfalls:
https://github.com/LozzKappa/claude-code-telegram-bot
— Khaled I.
Summary
Inbound
--channelsmessages enqueue into Claude Code's core in-memory command queue but are never flushed when the session is idle at the❯prompt — they surface as un-consumed editable "ghost text" in the input box, the agent stays idle, the Telegram poller stays409-healthy, and only a process restart clears it. This is the same bug as #44380, #36477, #61797. This report adds (1) binary-level confirmation of the mechanism and (2) the finding that a full client-side mitigation stack cannot fix it, because the stuck message is in the core's queue, downstream of any plugin.Environment
claude --channels plugin:telegram@claude-plugins-official --model sonnet --permission-mode bypassPermissions, run inside tmux (--channelsneeds a PTY)Symptom
A message sent at normal conversational cadence (a follow-up while the previous reply is generating, or within ~1–2s of it finishing — not flooding) intermittently lands as editable ghost text on the
❯line and is never processed. The session is idle (≈1% CPU, no spinner, footer← for agents).getUpdatesreturns 409 Conflict (the poller is alive and healthy), so a liveness watchdog is blind to it. Typing a character replaces the ghost text and backspace restores it (it's a queued command surfaced as editable input, not real buffer content), so manual Enter can't submit it. Only a process restart drains it. Longer/heavier turns (tool calls, long output) widen the window; trailing messages as a conversation winds down to idle are especially prone.Root cause — from the binary
The plugin delivers each inbound message fire-and-forget via
mcp.notification({ method:'notifications/claude/channel', params:{ content, meta } })(no ack, no pull). The core's handler does not inject it — it enqueues it into the core's own in-memory priority queue:On an idle session the REPL "prioritizes stdin over MCP notifications" (as #44380 already states), so the
priority:"next"channel item is never flushed andpopAllEditableleaves it as editable text at the prompt. The queue is in-memory only, so nothing persists it and a restart is the only thing that clears it.Verified locally on v2.1.161 by grepping
~/.local/share/claude/versions/2.1.161— these tokens are all present:popAllEditable,enqueuePendingNotification,getCommandQueueSnapshot,dequeueAllMatching,hasCommandsInQueue,notifications/claude/channel, and the exact literalnow:0,next:1,later:2.Why it isn't the plugin — and why client-side mitigation can't cure it
Reproduction (intermittent — it's a timing race)
claude --channels plugin:telegram@claude-plugins-official --model sonnetin tmux.❯prompt; the agent stays idle;getUpdatesreturns 409; only a process restart processes it. Trailing messages as the chat goes idle reproduce it most reliably.Suggested fix direction
On the idle transition, actively drain
priority:"next"channel items from the core command queue instead of leaving them un-flushed while prioritizing stdin — i.e., re-drive the queue when the session returns to the prompt, so a queued channel message starts a turn rather than sitting as editable ghost text.Related issues (verified open, 2026-06-03)
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.
Independent confirmation from a different transport. We run a custom stdio-MCP channel server (not the marketplace Telegram/Discord plugins) that emits
notifications/claude/channel, and we see identical behavior:<channel>notification is processed normally — the agent acts on it mid-turn.Because this reproduces with a custom server — a third independent channel implementation alongside the Telegram/Discord repros above — the fault is isolated to the REPL/harness idle path, not any one plugin; transport and emission are healthy.
Workaround we landed: don't rely on channel-inject for idle wake. A separate watcher that pokes the session with an external nudge is the only thing that reliably wakes an idle REPL for us; the channel push is kept for active-session delivery only.
Still reproduces on the current release. A maintainer ack / ETA would help — several of us are building on
--channelsas the intended idle-wake path (per #31854, closed as "implemented with Channels").There's a documented in-product path that does wake an idle session:
asyncRewake. It doesn't fix the notification drop discussed above, but it removes the keystroke leg from every workaround in this thread (tmuxsend-keys, pane scraping, F15 injection, nudge file followed by a fake Enter). It also sharpens the question in the title, because the product currently has two wake paths that behave differently on an idle session.What the docs already commit to
From the hook configuration field table in https://code.claude.com/docs/en/hooks:
And from Run hooks in the background → Limitations on the same page:
So the idle case is explicitly carved out on the rewake path, while
notifications/claude/channelgets enqueued and never flushed until a keystroke arrives (per the queue analysis posted upthread).One thing worth adding, because it answers "is this a real path or a curiosity": a first-party plugin already ships it.
security-guidance2.0.6, from the official marketplace, declaresasyncRewake: truetogether withrewakeMessageon aPostToolUsehook gated onBash(git commit:*), so its background security review can bring the model back to the findings. The field is in production use in Anthropic's own plugin, not just sitting in the reference table.Minimal repro, about five minutes
~/wake-probe.sh(probe quality, not production):~/.claude/settings.json:chmod +x ~/wake-probe.sh, start a session, ask anything trivial so a turn ends (theStopat the end of that turn is what arms the probe), then leave the session alone at the prompt. From an unrelated terminal:The session starts a turn on its own, with nothing written to its stdin.
What we measured
Claude Code 2.1.211, macOS, interactive TUI, no tmux, single run timed by wall clock. Treat these as an order of magnitude, not a benchmark:
<system-reminder>, matching the documentedstderr || stdoutbehaviour.UserPromptSubmit. We haven't seen that documented anywhere, and it was consistent on this version. Practical consequence if you build on this: an existingUserPromptSubmithook does the actual delivery on that turn, so the waker itself must never deliver, or the payload gets processed twice.Stop, which arms the probe again. The mechanism is self-sustaining, and just as self-looping if the probe ever exits 2 unconditionally.What it fixes and what it doesn't
It doesn't recover a notification the core queue already swallowed. The analysis upthread stands, and waking an idle session doesn't flush that queue. What it replaces is the fragile half of the existing workarounds. A channel server that already writes inbound messages to a file (two comments above propose exactly that) can pair that write with an
asyncRewakeStophook that wakes the session and says something is pending; the session then reads the file itself on the woken turn. No PTY, notmux send-keys, no pane regex, and it works in sessions that aren't running under tmux at all.Limits, stated plainly
rewakeMessageandrewakeSummarycontrol the wording the user and the model see. Neither appears in the hooks reference; both are visible insecurity-guidance'shooks.jsonand hook script. Treat them as unspecified and subject to change. The wake works without them, you just lose control of the wording, which defaults to "Stop hook feedback".args", which dedups two identical declarations rather than repeated firings of the same hook: "Each execution creates a separate background process, so multiple instances of the same hook can run concurrently if the event fires repeatedly." Since every turn arms one, the probe has to bound itself: single-flight keyed on thesession_idfrom the hook's stdin JSON, a hard TTL, exit when the owning session is gone, and announce a given item at most once. Otherwise a payload the session chooses to ignore loops forever, waking the model at whatever cadence the probe polls.asyncRewakedegrades to a synchronousStophook under single-shotclaude -p. A blocking probe there would stall the run until its timeout, so it should detect that case and exit 0 immediately.One question for maintainers
Is a
Stophook whose only job is to wake the session when external input arrives a supported use ofasyncRewake, or is the field meant to stay confined to the background-task failures the docs describe? And relatedly: why does the rewake path reach an idle REPL whilenotifications/claude/channelsits in the core queue until a keystroke? Knowing what makes one flush and the other not would tell everyone in this thread which path to build on, given that #31854 was closed as implemented with Channels.