channel-setup
Guided channel activation for local/tmux users — adds a channel entry when none is configured, installs the plugin, configures the bot token in the project-local state dir, and walks through pairing. Run after hatch, or any time to add or re-enable a channel.
- License 许可
- MIT
- 所属仓库
- gtapps/hermitd
- 最近更新
- 2026-11-15
- Stars
- ★ 85
- Forks
- 16
01 前言 Front Matter
- name
- channel-setup
- description
- Guided channel activation for local/tmux users — adds a channel entry when none is configured, installs the plugin, configures the bot token in the project-local state dir, and walks through pairing. Run after hatch, or any time to add or re-enable a channel.
- disable-model-invocation
- true
02 资源文件 1
- 参考文档references/group-enrollment.md4.3 KB
03 SKILL.md 正文
Channel Setup
Activate a channel, adding the config.json entry first when there isn't one. Local/tmux pairing is this skill's own flow; a Docker hermit is routed by the check below.
Commands
channel-group-add:bun ${CLAUDE_PLUGIN_ROOT}/scripts/channel-access.ts "<hermit_state_dir>" group-add
Plan
1. Read config and detect channels
Runtime routing (first): evaluate in this order.
-
Inside the container. Run
[ -f /.dockerenv ] || [ -f /run/.containerenv ] && echo container || echo host. Ifcontainer: read.hermit/config.json. For each enabled channel object, resolvestate_diras step 4 does (configstate_dir, default.claude.local/channels/<channel>; if relative, make it absolute against the project root) and print that absolute path literally. Print and stop:DM the bot for a code, then run
/hermitd:channel-setupfrom a normal host session at the project root. If no enabled channel, print only the host-run sentence and stop. NoAskUserQuestionon this path. -
No compose file. If
docker-compose.hermit.ymlis absent at the project root, continue with the local/tmux flow below. -
Live tmux hermit. Run
bun ${CLAUDE_PLUGIN_ROOT}/scripts/docker-preflight.ts "$(pwd)". If itslive_owneris non-null, a host tmux hermit owns this project despite the compose file: continue with the local/tmux flow below. -
Compose present. Build the stack in its precedence order,
COMPOSE_FILES=(-f docker-compose.hermit.yml); [ ! -f docker-compose.security.yml ] || COMPOSE_FILES+=(-f docker-compose.security.yml); [ ! -f docker-compose.override.yml ] || COMPOSE_FILES+=(-f docker-compose.override.yml), then rundocker compose "${COMPOSE_FILES[@]}" ps --status running --format '{{.Service}}'. Non-zero exit → print its output, stop. Ifhermitis absent from the output: after channel selection, run steps 2 and 4, skip 3, then stop withhermitd startand "re-run this skill to pair". Ifhermitis present: docker-running. -
docker-running. After channel selection, skip step 3 (the container installs channel plugins at boot). Run step 4; a
channel-bot-id.tsskip withhttp_401orhttp_403is reported as "token rejected by <platform>, fix it before restarting". If this run created the config entry or wrote the token → stop: "The bot is offline until the container restarts and loads it:hermitd restart, then DM the bot for a code and re-run this skill. Still no code after a restart:hermitd docker logs --tail=60shows the plugin's own error (wrong token, missing Message Content intent, install failure), and/hermitd:hermit-doctorchecks the token." Otherwise run §5, §6, §6a, §6b, and §6c against the host-visible channel<state_dir>, passing the absolute project.hermitdirectory as<hermit_state_dir>tochannel-access.ts. No restart prompt is needed for an already-running bot. Continue with §6d, §6e, then §7 for each paired channel. Add to the pairing question: "No code from the bot? It has not loaded the token:hermitd restart, then re-run this skill."
Read .hermit/config.json. Collect all entries under channels that are valid objects, tracking which are disabled (enabled: false).
Adding an entry. Where a branch below says create the entry, run (<name> is the lowercase channel key — discord, telegram — never the capitalized option label):
echo '{"channels":{"<name>":{"enabled":true}}}' | bun ${CLAUDE_PLUGIN_ROOT}/scripts/hatch-config.ts "$(pwd)" --reinit
The script fills enabled, default_chat_id: null, and state_dir (.claude.local/channels/<name>), validates the whole config, preserves every other key, and merges onto an existing entry rather than replacing it. It prints OK. On error nothing was written: report it and stop — never fall through into steps 2–6 against a channel that isn't in config.json.
- If no channels configured: offer to add one —
AskUserQuestion(header: "Channel") with options Discord, Telegram, Cancel. On Cancel, stop without writing. Otherwise create the entry and continue with that channel selected. Do not offer iMessage here: step 4 defines token vars for Discord and Telegram only. - If entries exist but all are disabled: name them, then ask with
AskUserQuestion(header: "Channel") — the disabled channel names plus Cancel — which to re-enable. On Cancel, stop without writing. Otherwise create the entry for the chosen name (this flipsenabledand leavesdefault_chat_id,state_dir, andallowed_usersintact) and continue with it selected. - If exactly one enabled channel and nothing disabled: use it automatically.
- Otherwise (several enabled, or enabled and disabled side by side): ask with
AskUserQuestion(header: "Channel") — every channel name as an option, disabled ones labelled<name> — disabled, plus All (enabled channels only) — which to set up. If a disabled name is chosen, create the entry for it first (re-enabling it), then continue with it selected.
Run steps 2–6 for each selected channel. On a Docker host (runtime routing 4–5), apply those overrides instead of the local/tmux default.
2. Check prerequisites
Run both checks in a single Bash call:
bun --version 2>/dev/null; uname -s
-
Bun missing (command fails / no output): tell the operator —
Bun is required for channel plugins but is not installed. Install: https://bun.sh Then re-run this skill.
Stop for this channel.
-
iMessage on non-macOS: if
uname -sis notDarwinand the channel isimessage, note it's macOS-only and skip this channel.
3. Install plugin
Run bun ${CLAUDE_PLUGIN_ROOT}/scripts/resolve-siblings.ts "$(pwd)" to get the project-or-local + enabled plugin list (JSON array; user-scope, managed, disabled, and cross-project entries already dropped). Each entry carries plugin, marketplace_name, scope, enabled.
Resolve the expected marketplace for this channel:
- If
channels.<channel>.marketplaceis set inconfig.json, use that value (third-party channel plugin path). - Otherwise, use
claude-plugins-official(built-in channels: discord, telegram, imessage).
Then check whether the surviving set contains an entry where:
- the plugin name (substring of
idleft of@) equals<channel>, AND - the marketplace name (substring of
idright of@) equals the resolved marketplace.
(Both clauses matter — discord@some-other-marketplace is a different plugin from a different source and must not satisfy this gate.)
- Found: skip silently. The channel plugin is already installed and enabled at project or local scope for this project — the canonical filter guarantees
enabled == true, and*_STATE_DIR(set byhermitd-startat boot) points at.claude.local/channels/<channel>/. - Not found: run, in order:
Explicitclaude plugin install <channel>@<marketplace> --scope local claude plugin enable <channel>@<marketplace> --scope localenablecovers the disabled-but-installed-at-project/local case — the filter excluded such entries (enabled-only), andinstallis a separate command fromenableper the CLI surface, so it may no-op without re-enabling. A user-scope install elsewhere does not satisfy this gate; channel tokens and access policy are project-local.
After any install (or if already present): tell the operator to run /reload-plugins in this session to activate the plugin's configure and access commands before pairing.
4. Token configuration (AskUserQuestion)
Before the token question, print the applicable platform prerequisite: Discord: Developer Portal, Bot tab: turn Message Content intent on; turn Public Bot off unless anyone may add it. Telegram: privacy mode only matters for respond-to-all; see §6c. No extra question.
Token env var names: discord → DISCORD_BOT_TOKEN, telegram → TELEGRAM_BOT_TOKEN.
Resolve state_dir:
- Read
channels.<channel>.state_dirfrom config.json. - If not set, default to
.claude.local/channels/<channel>. - If relative, it is relative to the project root (current directory).
Check if the token file already exists: <state_dir>/.env and contains the token var name.
- If yes: "Token already configured at
<state_dir>/.env." → proceed to step 5. - If no: display the official setup guide reference and ask for the token:
To create your bot and get a token, follow the official guide: https://code.claude.com/docs/en/channels
questions: [
{
header: "Bot token",
question: "Paste your bot token (or skip to add it later):",
options: [
{ label: "Skip", description: "I'll add the token to <state_dir>/.env manually" }
]
}
]
Operator pastes the token via Other, or selects Skip.
If token provided:
-
mkdir -p <state_dir> -
Write
<TOKEN_VAR>=<pasted-token>to<state_dir>/.env(overwrite if exists) -
chmod 600 <state_dir>/.env -
Ensure
.claude.local/is in.gitignore: check if.gitignoreexists and contains.claude.local/; if missing, append.claude.local/. -
If
channels.<channel>.state_dirwas not set in config.json (a legacy entry, or one reached through the single-enabled-channel branch that never ran Adding an entry), run the samehatch-config.ts … --reinitone-liner for<channel>now, but with an empty entry —echo '{"channels":{"<name>":{}}}' | …— so an existingenabled: falseis preserved (the{"enabled":true}payload would flip it). It fills the conventionalstate_dir, validates, and audits, leaving every other field intact. Never write the key with Edit/Write. -
Validate
<CHANNEL_UPPERCASE>_STATE_DIRfor the next resident start. Compute the absolute path ofstate_dir, then run:bun ${CLAUDE_PLUGIN_ROOT}/scripts/apply-settings.ts .claude/settings.local.json channel-env <CHANNEL_UPPERCASE> <absolute_state_dir>This validates and reports the state directory without writing settings. The configured state dir takes effect in the resident environment and launch overlay at the next start. Tokens stay in the channel's
.env. Confirm: "Configured<CHANNEL_UPPERCASE>_STATE_DIR→<absolute_state_dir>(takes effect at the next start)." -
Capture the bot's own identity so the hermit recognizes mentions of itself:
bun ${CLAUDE_PLUGIN_ROOT}/scripts/channel-bot-id.ts .hermit <channel> --writeWrites
bot_user_id(andbot_username) into the channel'sconfig.jsonentry, overwriting a stale value from a previous bot. Askipstatus is a non-event — report it and continue; setup never fails on it.
If token already configured: also run steps 6 and 7 before proceeding to step 5.
If Skip: print the manual command:
echo '<TOKEN_VAR>=your-token' > <state_dir>/.env && chmod 600 <state_dir>/.env
Then proceed to step 5 without a token (pairing will be skipped in step 5).
5. Restart + pairing (AskUserQuestion)
If no token is configured (skipped in step 4): print restart instructions and stop:
Restart Claude Code with channels active once you've added your token:
- With hermit:
hermitd-start(passes--channelsautomatically)- Manual:
claude --channels plugin:<channel>@<marketplace>(use the same<marketplace>resolved in step 3 —claude-plugins-officialfor built-in channels, orchannels.<channel>.marketplacefor third-party plugins.)
If token is configured: on docker-running go straight to the pairing question batch. Otherwise check whether the channel is already active in the current session by checking if the channel's reply tool is available. If active, skip the restart prompt and go straight to the pairing question batch.
If not active, display:
Token saved. Restart Claude Code to activate the channel:
- With hermit:
hermitd-start(passes--channelsautomatically)- Manual:
claude --channels plugin:<channel>@<marketplace>(use the same<marketplace>resolved in step 3.)After restarting, DM your bot: it will reply with a 6-character pairing code. Only people who may approve tool use should be DM-paired, because every
allowFromDM receives and can answer native permission prompts; everyone else joins through a group whose admission is independent ofallowFrom, andallowed_usersnarrows who can wake the hermit, never who can approve.
Then ask:
questions: [
{
header: "Pairing",
question: "Channel state?",
options: [
{ label: "Already paired", description: "Just verify access.json" },
{ label: "Ready to pair", description: "Restarted, DM'd the bot, have the 6-char code" },
{ label: "Skip", description: "I'll pair later" }
]
}
]
- Already paired: skip pairing, go to access.json verification in step 6.
- Ready to pair: proceed with pairing flow below.
- Skip: stop — "Run
/hermitd:channel-setupagain after restarting to complete pairing."
Pairing flow:
Ask with AskUserQuestion:
questions: [
{
header: "Pairing code",
question: "Paste the 6-character code your bot replied with:",
options: [
{ label: "Skip", description: "Pair later" }
]
}
]
If code provided (via Other), use the absolute project .hermit directory as <hermit_state_dir> (distinct from the channel's <state_dir>):
bun ${CLAUDE_PLUGIN_ROOT}/scripts/channel-access.ts "<hermit_state_dir>" pair <channel> <code>
bun ${CLAUDE_PLUGIN_ROOT}/scripts/channel-access.ts "<hermit_state_dir>" policy <channel> allowlist
Run the policy command only after pairing succeeds. Relay the result, retaining pairing's file (state, home or home+state) for §6. home: "set" means the paired DM became the briefing chat; kept leaves default_chat_id unchanged, possibly still empty. failed means pairing succeeded and the home chat is learned from the first message. On {"error"}, report the token and stop. Pairing and policy each raise the native permission prompt; bypass-mode callers are refused. Verify the location in §6.
If Skip: "DM the bot later, then run /hermitd:channel-setup from a normal host session." Stop.
6. Verify access.json location
Check if access.json exists at <state_dir>/access.json.
- If yes: done.
- If no: check
~/.claude/channels/<channel>/access.json. If found there, move it:
Confirm: "Moved access.json tomkdir -p <state_dir> mv ~/.claude/channels/<channel>/access.json <state_dir>/access.json<state_dir>/." - If found in neither location: note — "access.json not found. Pairing may not have completed. Run
/hermitd:channel-setupafter DMing your bot."
After file home or home+state, the bot that handed out the code still reads ~/.claude; the hermit reads the state directory, which home+state already updated and the move above covers otherwise. Use ${CLAUDE_CONFIG_DIR} instead of ~/.claude when that environment override is set.
If access.json is verified, continue to step 6a.
6a. Access control
Once per channel after verifying <state_dir>/access.json, read its allowFrom and the current channel config. Include iMessage: these settings are hermit-owned. Ask the row in one AskUserQuestion call, with the default option recommended:
| Header | Question | Options (label: description) |
|---|---|---|
| Access ctrl | Who may wake the hermit on this channel? | Only <paired id>: use the paired id from allowFrom (default) / Allow everyone: no hermit sender restriction / Other: additional user ids |
Drop Access ctrl when channels.<channel>.allowed_users is already present, preserving it. Use the paired id from allowFrom; if it is ambiguous or unavailable, have the operator identify the intended ids through Other instead of guessing. Encode selected ids as a string array, including the paired id for additional ids. For Allow everyone omit allowed_users.
Merge the answer through the existing reinit command:
echo '{"channels":{"<channel>":{"allowed_users":<selected_string_array>}}}' | bun ${CLAUDE_PLUGIN_ROOT}/scripts/hatch-config.ts "$(pwd)" --reinit
Omit allowed_users when the row was dropped or Allow everyone was chosen. Never use Edit/Write on config.json. Stop on a non-zero merge exit as in Adding an entry. Repeating the same answers must leave the file byte-identical. Continue to §6b.
6b. Default delivery settings
Once §6 verifies the channel state, skip iMessage; otherwise run:
bun ${CLAUDE_PLUGIN_ROOT}/scripts/channel-access.ts "<hermit_state_dir>" ensure-defaults <channel>
Relay ack (set or kept). The default is 👀 only when ackReaction is absent; an empty string and customized values are preserved. On {"error"}, report the token and stop.
6c. Server channel / group chat (optional)
Skip for iMessage or when pairing did not complete. Run the group questionnaire with the channel key, absolute <hermit_state_dir>, and native AskUserQuestion prompts. Carry forward the returned group answers, then continue to §6d.
6d. Maintainer channel check (optional)
If channels.<channel>.maintainer_channel_id is set in config.json, this channel routes technical, operational, and spend alerts to a second outbound-only destination (same bot/token, a different chat) instead of the primary chat, used on client-facing installs so the person on the primary chat never sees ops detail. Doctor's channel-liveness probe only checks the primary chat, so a typo'd maintainer id would otherwise fail silently on every alert rather than at setup.
When it's set, send one test message to the maintainer chat to confirm reachability:
bun ${CLAUDE_PLUGIN_ROOT}/scripts/channel-send.ts .hermit --tier maintainer --channel <channel> -
with a short line on stdin (e.g. "Maintainer channel check: technical and spend alerts will arrive here."). Record the result in the §7 summary: sent confirms the id; a failure means the id is wrong or the chat isn't reachable, so surface it so the operator fixes it now. If maintainer_channel_id is absent, skip silently.
A maintainer-send failure means the bot isn't present or permissioned in the target server/channel — the fix is to invite/permission the bot there. This chat is reached by a direct API POST with the bot token, never through access.json, so running /<channel>:access will not fix a failed send here.
6e. Recall, operators, primary channel, and recording
Once per selected channel, re-read config after the preceding merges. Ask the applicable rows in one AskUserQuestion call (at most four rows); drop conditional rows whose conditions are false. Skip the call if no rows remain. The defaults below describe a fresh setup; preserve existing customization when an answer is omitted.
| Header | Question | Options (label: description) |
|---|---|---|
| Recall scope | Which chats on this channel may this chat recall? | Each chat only itself: leave isolate_chats absent (default) / Every chat on this channel: set isolate_chats: false |
| Operators | Who may manage hermit-wide standing roles? | First allowed user (<id>): use the first allowed id (default) / Other: operator user ids |
| Primary | Which enabled channel should receive default outbound messages? | <this channel>: use this channel (default) / each other enabled channel by name |
| Record | Record chats on this channel for recall? | `Inherit global setting, currently <on |
Show Recall scope only if a group was added in §6c. Show Operators only if channels.<channel>.allowed_users contains at least two ids; for the default option omit operators (absence already means the first allowed user); for Other write the selected ids as the operators string array. Show Primary only if at least two channel objects have enabled: true and channels.primary is unset. Always show Record; read knowledge.channel_log_enabled to render the current global setting as on or off.
Merge the answered per-channel keys in one payload, omitting unselected keys and keys whose default is absence:
echo '{"channels":{"<channel>":{<answered_per_channel_keys>}}}' | bun ${CLAUDE_PLUGIN_ROOT}/scripts/hatch-config.ts "$(pwd)" --reinit
Never use Edit/Write on config.json. Stop on a non-zero merge exit as in Adding an entry. Omission preserves existing values on reinit; do not describe an existing override as cleared by omission. If a key is already customized, show that current value as the recommendation and preserve it unless an explicit replacement was selected. When the operator selects an absence default (Each chat only itself, Inherit global setting, First allowed user) for a key that is currently set, clear it with bun ${CLAUDE_PLUGIN_ROOT}/scripts/settings-edit.ts .hermit/config.json unset channels.<channel>.<key>; stop on a non-zero exit. Repeating the same answers must leave the file byte-identical.
For the Primary answer, run separately, substituting the selected enabled channel name:
bun ${CLAUDE_PLUGIN_ROOT}/scripts/settings-edit.ts .hermit/config.json set channels.primary <name>
Stop on a non-zero exit. Never include primary in a hatch-config.ts payload: that merger treats every entry under channels as a channel object. Continue to §7.
7. Summary
Channel setup complete!
Channel: <channel>
Plugin: installed (--scope local)
Token: configured (<state_dir>/.env)
Paired: yes / skipped
Server channels: <id1> (mention: yes/no), <id2> (mention: no) / skipped
State dir: <state_dir>
hermitd-start passes --channels automatically on next boot.
Later changes: /hermitd:hermit-settings channels (recall, record, operators)
When channels.<channel>.marketplace is set in config.json (third-party channel), also print this line after the block; never for built-in channels:
For additional custom channel integration, see the channel plugins guide: https://github.com/gtapps/hermitd/blob/main/plugins/hermitd/docs/channel-plugins.md#threads-and-notices
If anything was skipped, list the remaining steps.
7a. Send owner welcome (once, only when exactly one channel was newly paired)
Run this once, after step 7's summary, across the whole run — not per channel inside the steps 2–6 loop.
Skip entirely if no channel selected "Ready to pair" in step 5 this run — an operator re-running setup against an already-paired channel ("Already paired") or one they skipped shouldn't get a repeat welcome.
Also skip, with a note in the summary ("Welcome message skipped — more than one channel was newly paired this run; let the owner know directly"), if more than one channel selected "Ready to pair" this run (the "All" path pairing several channels at once). channel-send.ts has no per-channel target — it resolves one generic outbound channel (primary, else first eligible in config order) — so with several freshly-paired channels there's no reliable way to know which one it would reach.
Otherwise (exactly one channel newly paired), compose a short welcome in the operator's configured language (config.json), in your own voice, so the owner has something the moment the bot can reach them. Not the full guide, just an orientation pointer covering: they can talk to you anytime in plain language; when you have a suggestion or need a decision you will ask, and yes, later, or no is enough; !pause stops you until !resume (both must start with !); you track AI spend and warn when it nears any configured limit; if you go quiet or something is confusing, they should reach whoever set you up (who also has the full written guide). No file paths or internal jargon: it's the owner's first message, and the only chat commands allowed are the five control commands !pause, !stop, !resume, !snooze, !status. Send it on stdin:
bun ${CLAUDE_PLUGIN_ROOT}/scripts/channel-send.ts .hermit - <<'HERMIT_WELCOME'
<the composed welcome>
HERMIT_WELCOME
Use this send as the post-hatch delivery confirmation and report it explicitly in the summary. On success: "Test message sent — check <channel> and tell me if it didn't arrive." If the send fails, don't block setup: "Channel paired, but the test message didn't arrive (<error>) — check the pairing before relying on this channel."
04 同仓库更多 Skills
dream
Nightly memory maintenance from transcript ground truth. Verifies, enriches and tidies auto-memory and topic pages with snapshot validation and undo. Also handles 'undo the dream'.
when-done-switch-to
Arm a deferred model or effort switch. Use for "when done switch to", "after this drop to sonnet", or "switch model when idle".
weekly-review
Generate the weekly review report for the current ISO week. Writes to .hermit/compiled/review-weekly-YYYY-Www.md (dev-facing detail) and sends a plain-language channel summary (Delivered / Decisions / Waiting on you / Spend). Runs every Sunday at 23:00 via routine.
watch
Background events via the config watch monitor or ad-hoc Monitor tool. Quiet watches cost zero tokens.
watch-activate
Activate the config watch monitor.
voice-notes
Transcribe and respond to voice notes. Activates when any channel message carries an audio attachment, or when asked to transcribe an inbound voice message.