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.
Bridges Claude Code sessions to Telegram, Discord, Feishu, QQ, or WeChat for mobile chat. Manages setup, start, stop, and diagnostics of the bridge daemon.
Diagnoses and repairs Discord bot connectivity, Gateway daemon status, OAuth token validity, and legacy Clawdis configuration conflicts.