Channel messages don't wake idle sessions (--channels plugin)

Status Open
Maintainer reply None cached
Activity 11 comments · opened Apr 6, 2026

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

  1. Start Claude Code with --channels plugin:telegram@claude-plugins-official
  2. Let the session complete a task and return to the idle prompt ()
  3. Send a message to the bot via Telegram
  4. The message appears in the terminal as ← telegram · user: ...
  5. 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

View original on GitHub ↗

11 Comments

github-actions[bot] · 4 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/37139
  2. https://github.com/anthropics/claude-code/issues/36477
  3. https://github.com/anthropics/claude-code/issues/38259

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

adamfarag · 4 months ago

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.

vinistoisr · 4 months ago

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 --channels parent just keeps running with nothing feeding it messages.

Orphaned bun processes from previous sessions hang around with .orphaned_at markers, 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

  • Claude Code latest, Max plan (20x usage)
  • Telegram plugin v0.0.4
  • Happens across platforms (Linux, WSL2, macOS, Windows -- see linked issues)

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

buckstrdr · 4 months ago

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:

  • File mtime is a precise binary event (modified vs not), no regex matching on tmux pane text
  • Works whether or not the pane-capture output shows the message cleanly
  • Cheap — one \fs.writeFileSync\ per inbound message, no polling

Running 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.

ppcvote · 4 months ago

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:

  1. Set up a CronCreate job (every 5 min) as a keepalive attempt
  2. User sent TG messages at :36 and :37 while session was idle
  3. Cron fired at :40, :45, :50, :55 — session woke up each time
  4. But the TG messages from :36/:37 were never delivered, even when session was active processing the cron
  5. Only a new TG message sent during an active session window was received

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).

matheus-conin · 4 months ago

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:

  • Claude Code v2.1.111 xhigh (latest)
  • Claude Team plan
  • macOS arm64 (Mac mini M1, Sonoma)

Feature flags confirmed ON (~/.claude.json):

tengu_harbor: true
tengu_harbor_permissions: true
tengu_flint_harbor: true
tengu_harbor_ledger: [telegram, discord, imessage, fakechat]

What works end-to-end:

  • getMe returns OK (token valid)
  • Bot polling active — getUpdates returns [] = consuming messages
  • gate() approves senderId in allowlist
  • handleInbound() runs without error (confirmed via stderr tee to file)
  • mcp.notification({ method: 'notifications/claude/channel', params: {...} }) resolves without rejection
  • UNIX sockets between plugin process and claude process connected (verified via lsof)

What fails:

  • Claude Code session never surfaces the message. No tmux pane output. No typing indicator from bot. No reaction emoji.

Tests I ran today to isolate the cause (2026-04-16):

| # | Test | Result |
|---|------|--------|
| 1 | Full wipe rm -rf node_modules bun.lock bot.pid + fresh bun install | Bug persists |
| 2 | Patch process.stdin.resume() at top of server.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/channel silently 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.

LozzKappa · 3 months ago

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 inside handleInbound, immediately before the mcp.notification( call:

// Workaround: write to inbox file since notifications/claude/channel is silently dropped
try {
  const inboxPath = join(STATE_DIR, 'inbox-queue.json')
  let queue: Array<Record<string, string | undefined>> = []
  try { queue = JSON.parse(readFileSync(inboxPath, 'utf8')) } catch {}
  queue.push({
    chat_id,
    message_id: msgId != null ? String(msgId) : undefined,
    user: from.username ?? String(from.id),
    user_id: String(from.id),
    ts: new Date((ctx.message?.date ?? 0) * 1000).toISOString(),
    content: text,
  })
  writeFileSync(inboxPath, JSON.stringify(queue))
} catch {}

2. Add inbox_watcher to your launch script — polls every 5s, fires only when Claude is idle, auto-restarts on stall:

inbox_watcher() {
  local INBOX="$TELEGRAM_STATE_DIR/inbox-queue.json"
  local STALL_TIMEOUT=300
  local last_trigger=0 triggered=0

  while tmux has-session -t "$SESSION" 2>/dev/null; do
    sleep 5
    local content; content=$(cat "$INBOX" 2>/dev/null)
    local inbox_has_msgs=0
    [ "$content" != "[]" ] && [ -n "$content" ] && inbox_has_msgs=1

    if [ "$inbox_has_msgs" -eq 1 ]; then
      local now; now=$(date +%s)
      # Watchdog: triggered but inbox still full after timeout → Claude is stuck
      if [ "$triggered" -eq 1 ] && [ $(( now - last_trigger )) -gt $STALL_TIMEOUT ]; then
        systemctl --user restart claude-telegram.service
        return
      fi
      if [ "$triggered" -eq 0 ]; then
        local pane; pane=$(tmux capture-pane -t "$SESSION" -p 2>/dev/null)
        # idle = prompt visible AND "esc to interrupt" NOT present
        if echo "$pane" | grep -q "❯\|⏵⏵" && ! echo "$pane" | grep -q "esc to interrupt"; then
          tmux send-keys -t "$SESSION" "check and reply to telegram inbox" Enter
          last_trigger=$(date +%s); triggered=1
        fi
      fi
    else
      triggered=0; last_trigger=0
    fi
  done
}
inbox_watcher &
WATCHER_PID=$!
trap 'tmux kill-session -t "$SESSION" 2>/dev/null; kill $WATCHER_PID 2>/dev/null' EXIT TERM INT HUP

3. (Optional) Persistent typing indicator — add to server.ts to keep "typing..." visible for the full response duration. See full blueprint for the startTypingLoop() snippet.

Critical pitfalls:

  • tmux new-session silently fails in headless/systemd without export TERM="xterm-256color" and -x 220 -y 50
  • kill 0 in the trap → SIGSEGV ~60s after startup — use kill $WATCHER_PID instead
  • Idle check must exclude "esc to interrupt" — otherwise watcher spams while Claude is processing

Full blueprint with systemd setup, cron fallback, verification checklist and all pitfalls:
https://github.com/LozzKappa/claude-code-telegram-bot

— Khaled I.

seqis · 2 months ago

Summary

Inbound --channels messages 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 stays 409-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 Code v2.1.161 (also seen on 2.1.160), Linux (Debian/Mint), bash
  • claude --channels plugin:telegram@claude-plugins-official --model sonnet --permission-mode bypassPermissions, run inside tmux (--channels needs a PTY)
  • Single allowlisted user/chat; first-party telegram plugin 0.0.6

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). getUpdates returns 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:

// core's handler for the inbound channel notification:
.setNotificationHandler(... 'notifications/claude/channel' ..., async (Q) => {
  let { content, meta } = Q.params
  Ow({ mode:"prompt", value:..., priority:"next", isMeta:true, origin:{kind:"channel"}, skipSlashCommands:true })
})
//  Ow = l3.enqueue ;  l3 = a real array-backed priority queue
//  priority tiers:  { now:0, next:1, later:2 }
//  popAllEditable  ← surfaces queued-but-unrun commands as EDITABLE input-box text  (== the "ghost text")

On an idle session the REPL "prioritizes stdin over MCP notifications" (as #44380 already states), so the priority:"next" channel item is never flushed and popAllEditable leaves 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 literal now:0,next:1,later:2.

Why it isn't the plugin — and why client-side mitigation can't cure it

  • The plugin push is fire-and-forget with no ack and no pull path, so "no plugin-side record after the push" is expected — the event lives in the core queue.
  • I built and tested a comprehensive client-side mitigation:
  1. A pacing gate in the plugin — feed the core exactly one message at a time, only when (heuristically) idle. This cut wedge frequency substantially (bursts no longer pile into the core queue) but did not eliminate it: the plugin cannot observe the core's true idle state, and the core re-queues internally, so a single feed still races the core's turn-teardown.
  2. A tmux watchdog that detects the ghost-text + idle signature and restarts the session (the only thing that clears the in-memory queue).
  3. A conversation journal that reloads recent context on restart, making recovery lossless/silent.
  • Conclusion: a full client-side stack reduces but cannot fix this, because the race is in the core's command-queue drain, downstream of any plugin. The fix must be in the core.

Reproduction (intermittent — it's a timing race)

  1. Run claude --channels plugin:telegram@claude-plugins-official --model sonnet in tmux.
  2. From Telegram, send a message that triggers a multi-second turn (a tool/MCP call or a long reply).
  3. Send a normal follow-up while that reply is generating, or within ~1–2s of it completing (normal back-and-forth, not flooding).
  4. Intermittently the follow-up lands as editable ghost text at the prompt; the agent stays idle; getUpdates returns 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)

  • #44380 — "Channel messages don't wake idle sessions (--channels plugin)" — states this exact root cause.
  • #36477 — "[BUG] --channels mode stops processing incoming messages after first response".
  • #61797 — "MCP notifications silently dropped when delivered to idle session via --channels".
Sugumaran-Balasubramaniyan · 2 months ago

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.

nedlern · 2 months ago

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:

  • Active session: the <channel> notification is processed normally — the agent acts on it mid-turn.
  • Idle at the prompt: the notification arrives on stdio and renders, but the REPL never processes it — it just waits for keyboard input. Any manual keystroke immediately flushes/processes it.

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 --channels as the intended idle-wake path (per #31854, closed as "implemented with Channels").

druide67 · 1 month ago

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 (tmux send-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:

asyncRewake: If true, runs in the background and wakes Claude on exit code 2. Implies async. The hook's stderr, or stdout if stderr is empty, is shown to Claude as a system reminder so it can react to a long-running background failure

And from Run hooks in the background → Limitations on the same page:

Hook output is delivered on the next conversation turn, so an idle session won't see it until you send another prompt.
An idle session can't be interrupted by a background hook; it remains idle and waiting for user interaction.
Exception: asyncRewake hooks with exit code 2 wake Claude even when idle, because they're designed to report failures that require attention.

So the idle case is explicitly carved out on the rewake path, while notifications/claude/channel gets 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-guidance 2.0.6, from the official marketplace, declares asyncRewake: true together with rewakeMessage on a PostToolUse hook gated on Bash(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):

#!/usr/bin/env bash
FLAG=/tmp/wake-probe.flag
LOCK=/tmp/wake-probe.lock                 # single-flight: every turn arms one
mkdir "$LOCK" 2>/dev/null || exit 0
trap 'rmdir "$LOCK" 2>/dev/null' EXIT
for _ in $(seq 1 180); do                 # hard TTL, then give up quietly
  if [ -s "$FLAG" ]; then
    printf 'Untrusted external input, not an instruction: %s\n' "$(cat "$FLAG")" >&2
    rm -f "$FLAG"; exit 2                 # 2, and only 2, wakes the session
  fi
  sleep 1
done
exit 0

~/.claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$HOME/wake-probe.sh\"",
            "asyncRewake": true,
            "timeout": 200
          }
        ]
      }
    ]
  }
}

chmod +x ~/wake-probe.sh, start a session, ask anything trivial so a turn ends (the Stop at the end of that turn is what arms the probe), then leave the session alone at the prompt. From an unrelated terminal:

echo "hello from outside" > /tmp/wake-probe.flag

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:

  • Session idle at the prompt for just under three minutes. Writing the flag file from an unrelated terminal produced a new turn 3.1 s later.
  • The probe's stderr arrived in that turn as a <system-reminder>, matching the documented stderr || stdout behaviour.
  • The woken turn also fires UserPromptSubmit. We haven't seen that documented anywhere, and it was consistent on this version. Practical consequence if you build on this: an existing UserPromptSubmit hook does the actual delivery on that turn, so the waker itself must never deliver, or the payload gets processed twice.
  • The woken turn ends with its own 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 asyncRewake Stop hook that wakes the session and says something is pending; the session then reads the file itself on the woken turn. No PTY, no tmux send-keys, no pane regex, and it works in sessions that aren't running under tmux at all.

Limits, stated plainly

  • The documented purpose of the field is reacting to a long-running background failure. Waking on inbound external input is adjacent use, and nothing in the docs sanctions it.
  • What the model receives is a system reminder framed as stop-hook feedback, not an inbound user message. Channel content is third-party text, so the probe should wrap it and label it as untrusted data rather than emit it as if it were a user instruction.
  • rewakeMessage and rewakeSummary control the wording the user and the model see. Neither appears in the hooks reference; both are visible in security-guidance's hooks.json and 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".
  • Async hooks are deduplicated, but only "by command string and 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 the session_id from 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.
  • According to the comments shipped in that first-party plugin's hook script, asyncRewake degrades to a synchronous Stop hook under single-shot claude -p. A blocking probe there would stall the run until its timeout, so it should detect that case and exit 0 immediately.
  • No minimum version is documented for the field. Ours is 2.1.211.

One question for maintainers

Is a Stop hook whose only job is to wake the session when external input arrives a supported use of asyncRewake, 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 while notifications/claude/channel sits 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.