Skip to content
Automation
Skill

/channel-responder

Handles inbound messages from Claude Code Channels (Telegram, Discord, webhooks) with session context awareness.

From plugin
claude-code-hermit
7488 skills11 agents
Install
$ npx -y skills add gtapps/claude-code-hermit --skill channel-responder --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/channel-responder

Context preview

The summary Claude sees to decide when to auto-load this skill.

Handles inbound messages from Claude Code Channels (Telegram, Discord, webhooks) with session context awareness.

SKILL.md

channel-responder.SKILL.md
name: channel-responder
description: Handles inbound messages from Claude Code Channels (Telegram, Discord, webhooks) with session context awareness.

Channel Responder

When a message arrives via a channel:

0. Reply via the channel

Every response to a message wrapped in `<channel source="..." chat_id="..." ...>` goes through the channel's reply tool, not the terminal/transcript. Terminal output is invisible to the operator: they read Discord, Telegram, or the configured channel, never the raw transcript.

For each channel plugin, the reply tool is the `reply` action exposed by the channel's MCP server, named `mcp__plugin_<plugin-name>_<server-name>__reply` — the two segments are exactly the plugin name and server name the harness puts in the plugin-qualified source on the wire (`source="plugin:<plugin-name>:<server-name>"`). For the built-in channels the two coincide (e.g. `plugin:discord:discord` → `mcp__plugin_discord_discord__reply`), but a custom channel plugin whose names differ fills each slot from its own wire segment (e.g. `plugin:acme-crm:crm` → `mcp__plugin_acme-crm_crm__reply`) — build the tool name from the raw `source`, not by doubling one segment. Every `config.channels` key below instead uses the normalized bare server name (`discord`, not the qualified string — see `lib/channel-envelope.ts`'s `normalizeChannelSource`). When only that bare `<sourceKey>` is available (a `later` row's `chat`, a conversation binding key), the reply tool is the loaded `…__reply` tool whose server segment is exactly `<sourceKey>` (`mcp__plugin_<plugin-name>_<sourceKey>__reply`); when none matches, or more than one does, the chat is unreachable and the undelivered message is reported per § Operator Notification instead of replying. Pass the inbound `chat_id` back. Optionally pass `reply_to` (the inbound `message_id`) to thread under the operator's message. The tool result names the sent message (`sent (id: N)`); the same plugin's `edit_message` tool rewrites that message in place. A channel whose tool list has no `edit_message` gets short threaded replies wherever the rules below say to edit a progress card, and no `Progress card` line is recorded for it.

Terminal output is acceptable as a SECONDARY surface (tool-call narration, status visible only to a maintainer at the box). Every response the operator needs to see, a short acknowledgement included, must go through the channel.

**Exception, checked first.** When this turn's context carries a `[harness-command] … requested` line, stop: no tool call (§1–§1d included) and no reply; the reason is in §2's Harness command bullet. A `[harness-command] refused "…"` line is the opposite case: nothing was recorded and the operator is owed the reason, so reply as usual.

Message formatting

When preparing a channel send, preserve the intended message content when encoding the tool arguments. Apply only the escaping required by the selected tool and rendering mode. Do not add or remove escaping within quoted code, HTML examples, or other literal content. Before sending, compare the final message body with the intended text. Normal JSON encoding still applies. This check concerns only the message body, not generated artifacts, source files or attachments; it does not change the tool's rendering mode or add mention support.

1. Load Context

Treat `MEMORY.md` hook lines tagged `[role]` as hermit-wide instructions for this turn, and lines tagged `[role <key>:<chat_id>]` as instructions only when `<key>` is this channel's normalized bare key from §1c (`discord`, not `plugin:discord:discord`) and `<chat_id>` matches this message's `chat_id`. A role applies only to a message addressed to you: in a 1:1 DM every message is, and in a group or server chat one that mentions you (`bot_user_id`/`bot_username`, the same self-mention test §2 uses for addressed commands). Silently ignore roles pinned to another chat without mentioning them in the reply; the hook line is sufficient, with no topic-file Read.

Read `.claude-code-hermit/sessions/SHELL.md` for current task context. Read `state/runtime.json` for lifecycle state (`session_state` is the source of truth — never parse SHELL.md `Status:` for decisions).

Before the state check, apply **Micro-approval response** below first to a bare yes/ok/no with a pending micro-proposal. Otherwise read `TASKS.md` and run `bun ${CLAUDE_PLUGIN_ROOT}/scripts/task.ts list .claude-code-hermit --open --conversation <sourceKey>:<chat_id>`. An open handle, bound task thread, or continuation of the sole open task selects it. With multiple open tasks and neither handle nor thread, ask one short question naming the handles and record nothing. Plain questions open nothing.

For a selected task, confirmation of its posted result uses `bun ${CLAUDE_PLUGIN_ROOT}/scripts/task.ts close .claude-code-hermit <id> --by confirmed --actor <sourceKey>:<user_id> --result-rev <current> --reason-stdin`; pipe the confirmation words. Cancel uses `bun ${CLAUDE_PLUGIN_ROOT}/scripts/task.ts cancel .claude-code-hermit <id> --actor <sourceKey>:<user_id> --reason-stdin`. Changed done criteria use `bun ${CLAUDE_PLUGIN_ROOT}/scripts/task.ts note .claude-code-hermit <id> --actor <sourceKey>:<user_id> --done <definition>`; steering pipes a line into `bun ${CLAUDE_PLUGIN_ROOT}/scripts/task.ts note .claude-code-hermit <id> --actor <sourceKey>:<user_id>`. Authorization remains §1c, including a named approver for confirmed closure. Show handles only for two or more open records in this conversation; in DMs also require `config.tasks.handle_in_dm`.

1b. Check Session State

A bound conversation, or a task eligible for **Bind** in §2, never adopts or replaces the resident's SHELL.md task. For those messages, check the shutdown condition below and §1c authorization, then go directly to §2 before the resident's waiting/recovery choices. Do not run the resident task-state mutations below for a conversation helper.

If runtime.json `session_state` is

Read more
Ships withclaude-code-hermit

If you know Claude Tag, the idea will feel familiar: hand Claude work through a channel, such as Discord, Telegram, or your custom integration, and get results back there.

Get the whole plugin

Other skills on claude-code-hermit.