From Dashi Channel — Telegram bridge for Claude Code
Diagnoses and safely completes migration from legacy Telegram gateway to dashi-plugin-claude-code channel. Handles silent bots, 409 conflicts, and cutover planning.
How this skill is triggered — by the user, by Claude, or both
Slash command
/dashi-channel:doctor-dashi-pluginThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill helps an agent move from the **old gateway** (a Python daemon that
This skill helps an agent move from the old gateway (a Python daemon that
spawns a fresh claude -p session for every Telegram message) to the new
channel plugin (one long-running Claude session per agent), and diagnose what
went wrong if the migrated bot misbehaves.
Why migrate at all: the old gateway hits a permission-dialog hang without a
bypass flag, has no guarantee the final answer is sent, and — past 2026-06-15
— becomes expensive because per-message claude -p spawn moves to a separate
billing pool. The plugin keeps one live session inside the Max subscription.
The doctor is read-only. It never restarts a service, never writes config, never prints a secret. It tells you what is wrong and points at the fix; you (or the operator) apply it.
Never restart your own channel/gateway from inside your own session. That
kills the only comms link mid-turn, and the supervisor relaunches you into a
loop. Apply config changes without restarting (the old process keeps running old
code), and let an external actor do the restart — the operator in their
terminal, or another service. This is non-negotiable; see
references/03-lessons-learned.md §3.
# On a systemd host this is usually ALL you need — the doctor finds the
# channel-*.service unit whose WorkingDirectory matches the plugin dir and
# infers --env / --session / the state config from it (autodetect):
bun skills/doctor-dashi-plugin/scripts/doctor.ts --plugin-dir <...>/plugin
# Explicit flags always win over autodetect; pass them when the layout is unusual:
bun skills/doctor-dashi-plugin/scripts/doctor.ts \
--plugin-dir <workspace>/.claude/dashi-plugin-claude-code/plugin \
--settings <plugin-dir>/.claude/settings.json \
--mcp <plugin-dir>/.mcp.json \
--settings-local <plugin-dir>/.claude/settings.local.json \
--env <channel.env path> \
--user <your numeric Telegram id> \
--session channel-<agent> \
--no-autodetect # opt out of systemd unit discovery
Every flag is optional — pass what you have. --json gives machine-readable
output for an agent to parse. Exit code: 0 = no FAIL, 1 = at least one FAIL,
2 = usage error. Settings default to <plugin-dir>/.claude/settings.json
when it exists (the session-cwd layout Claude Code actually loads hooks from),
falling back to ~/.claude/settings.json.
Multi-agent fleet mode (README section 14): add --fleet to discover every
channel-*.service unit on the host (helper units without a tmux Exec line are
skipped) and verify the five isolation invariants ACROSS agents — unique
webhook ports, unique bot tokens (sha256 digests, never printed), a dedicated
tmux socket per unit (duplicates incl. two-on-default fail; a single default
socket is a warn), the shared ~/.claude/settings.json free of channel hooks,
webhook.enabled=true in every agent's state config, and each agent's hooks
pointing at its OWN webhook port (the last-install-wins disaster). Use
--fleet-dir <dir> for a non-standard unit directory or in tests. Run it
before and after adding agent #2..N.
The doctor checks, in order: toolchain floors (Claude Code ≥ 2.1, Bun ≥ 1.3.9,
tmux), workspace placement (the #1 first-run failure — identity drift),
systemd autodetect, dev-vs-runtime copy (multi-agent aware: matches the running
server by CWD across ALL plugin processes on the host), settings selection +
hooks + token leak, the hook profile (a tmux-mirror-only setup is NOT
punished for the feeder hooks it deliberately lacks; feeders are demanded only
when the state config enables a hook-driven status/progress surface), the
permission gate deep checks (ask-relay registered next to the gate, the
policy file the gate points at exists, confirm_overrides does not lift
sudo/rm -rf, default tier, the unit carries --permission-mode bypassPermissions), multichat (per-chat policy parses, terminal mirror is
DM-only — a public chat with tmux_mirror: true is a FAIL, every chat dir has
a policy entry, spawn-chat-shell forwards TMUX_PANE), security hygiene
(the hook webhook must listen on loopback ONLY — a 0.0.0.0 bind is a FAIL;
the channel env file holding the bot token must not be world-readable; the
user-level ~/.claude/settings.json must be free of channel hooks), MCP
comms consistency (the latent silent-channel landmine — now checked against
the plugin-dir .mcp.json/settings.local.json by default), the Telegram
allowlist, and live-session signals (welcome-prompt hang, expired auth, crash
loop).
Checks are ordered by causality: a failure early on explains failures later, so
fix the first FAIL, re-run, repeat. Don't chase a downstream symptom while an
upstream cause is still red. Each FAIL/WARN carries a one-line fix: pointer.
references/01-migration-steps.md
(pre-flight, ordered steps, the cutover order, rollback, Linux vs macOS).references/02-failure-modes.md (problems 1–10: symptom → root cause → fix).references/03-lessons-learned.md
(dev-vs-runtime copies, comms-config breakage, self-restart loop, token leak,
fallback-reply, FTS, push-on-assign, and more — each with a detection check).A green doctor run is necessary, not sufficient — the only proof is a real
message. Send the bot a message and watch for the reaction flow 👀 → ⚙️ → ✅
and an actual reply. If silent, diagnose by architecture, not by symptom
(service status → tmux capture → identity → only then the Telegram queue); the
order matters and is spelled out in references/02-failure-modes.md (Problem 4).
bot silent after cutover?
├─ service "active" but no reactions at all
│ └─ welcome-prompt hang → attach tmux, press Enter (Problem 1)
├─ reactions (👀) but no text reply
│ └─ OAuth expired → attach tmux, /login (Problem 7)
├─ logs show "409 Conflict"
│ └─ two consumers on one token → stop the channel, wait 45s, re-test;
│ if 409 persists the second consumer is EXTERNAL — hunt it (Problem 3, §4)
├─ answers but as "default Claude"
│ └─ wrong WorkingDirectory → CLAUDE.md unreachable (Problem 2)
├─ pending=0, no reply, no error
│ └─ allowlist drops your id (Problem 5)
└─ service "activating (auto-restart)", exit 0/SUCCESS, tmux gone
└─ crash loop — run claude by hand with a TTY to see the real error (Problem 9)
The diagnostic is a single standalone file, scripts/doctor.ts, with no
plugin-internal imports (so it keeps working even when the plugin checkout is
broken — diagnosing that is one of its jobs). Pure check functions are exported
and unit-tested in scripts/doctor.test.ts (bun test). Add a new check as a
pure function returning a Check, wire it into gatherChecks, and add a test.
Keep every probe read-only and run all output through redact().
npx claudepluginhub qwwiwi/dashi-plugin-claude-codeDiagnoses, debugs, deploys, and monitors Telegram bots with structured health checks, webhook/polling diagnostics, environment validation, and safe restart checklists.
Diagnoses Telegram bot connection errors using error codes (403, 400, 401, 429) and guides fixes for token, chat_id, network, and permission issues.
Bridges Claude Code sessions to Telegram, Discord, Feishu, QQ, or WeChat for mobile chat. Manages setup, start, stop, and diagnostics of the bridge daemon.