Built-in channel-reply guardrail for channel-routed plugins (e.g. Telegram)

Resolved 💬 1 comment Opened May 16, 2026 by alemedin Closed Jun 15, 2026

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

  1. Run claude --channels plugin:telegram@claude-plugins-official and pair the bot.
  2. Send 4–6 messages from the channel side, asking questions that elicit long structured replies (tables, multi-section analyses).
  3. Observe Claude producing responses in the CLI transcript but intermittently failing to fire the mcp__plugin_telegram_telegram__reply tool.
  4. 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:

  1. Scans the current session transcript (.jsonl) for the most recent inbound channel event (a type:queue-operation line whose content contains a <channel source="..."> tag).
  2. Checks every line after that point for an assistant tool_use whose name matches the channel plugin's registered reply tool.
  3. 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 replyToolName field 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-config skill description (the doctrine that this implements)
  • Existing channel plugin: plugin:telegram:telegram and the reply/react/edit_message/download_attachment tools
  • Hook events: Stop is the natural choice; SubagentStop may also apply for delegated work

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗