Built-in channel-reply guardrail for channel-routed plugins (e.g. Telegram)
Problem
When Claude Code is bound to a channel-routed plugin — for example claude --channels plugin:telegram@claude-plugins-official — the agent receives inbound messages via the channel and is instructed (in the plugin's MCP instructions) to respond via a specific reply tool (e.g. mcp__plugin_telegram_telegram__reply).
The model's default output mode is text into the session transcript, which does not reach the channel user. When the agent forgets to wrap a response in the reply tool call, the user receives nothing — a silent failure with no visible error to either side.
This is not a plugin bug. It's a behavior failure mode: the reply-tool requirement is a soft instruction in the system prompt that the model can and does drop, especially on long structured responses or in long sessions where the instruction drifts out of attention.
Reproduction
- Run
claude --channels plugin:telegram@claude-plugins-officialand pair the bot. - Send 4–6 messages from the channel side, asking questions that elicit long structured replies (tables, multi-section analyses).
- Observe Claude producing responses in the CLI transcript but intermittently failing to fire the
mcp__plugin_telegram_telegram__replytool. - The channel user sees long silent gaps, eventually asks "why no response?"
Personally hit this 4+ times in a single session even after explicitly acknowledging the failure mode mid-conversation. Memory/feedback alone does not reliably fix it.
Proposed Solution
Ship a built-in Stop hook (opt-in via channelsEnabled: true, or auto-enabled for any session bound with --channels) that:
- Scans the current session transcript (
.jsonl) for the most recent inbound channel event (atype:queue-operationline whosecontentcontains a<channel source="...">tag). - Checks every line after that point for an assistant
tool_usewhosenamematches the channel plugin's registered reply tool. - If a channel arrival exists with no subsequent reply tool call, returns:
``json``
{"decision":"block","reason":"Channel message unanswered — call the reply tool before ending the turn."}
to force the model to send the reply before turn-end.
Per the update-config skill's own doctrine: "automated behaviors require hooks configured in settings.json — the harness executes these, not Claude, so memory/preferences cannot fulfill them." This is exactly that.
Implementation Reference
Built this manually for the Telegram plugin in ~60 lines of bash. Generic version would need:
- Reply-tool-name registration in the channel plugin manifest (e.g. a
replyToolNamefield that the hook reads). - Per-plugin or per-session opt-out for cases where Claude legitimately shouldn't reply (e.g. a user explicitly said "don't respond").
- Probably a one-line addition to plugin-install flow so users get the guardrail automatically when they pair a channel plugin.
Why It Matters
Without this, channel-routed sessions — which are the Claude Code feature most likely to be used as an "always-on assistant" pattern — are unreliable in a way that's invisible until it breaks trust. The user only finds out by noticing silence and asking. By that point you've burned a chunk of credibility.
A built-in guardrail makes channel integration production-grade by default, instead of every user needing to discover the failure mode and write their own hook.
Related
update-configskill description (the doctrine that this implements)- Existing channel plugin:
plugin:telegram:telegramand thereply/react/edit_message/download_attachmenttools - Hook events:
Stopis the natural choice;SubagentStopmay also apply for delegated work
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗