{"id":51708630,"url":"https://github.com/2naive/claude-telegram-topics","last_synced_at":"2026-07-25T10:30:27.613Z","repository":{"id":369820346,"uuid":"1291657572","full_name":"2naive/claude-telegram-topics","owner":"2naive","description":"Telegram channel for Claude Code with 1 project = 1 forum topic. Concurrent multi-project support via a leader/follower bridge. Clean fork of the official Anthropic Telegram plugin.","archived":false,"fork":false,"pushed_at":"2026-07-22T21:59:32.000Z","size":451,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-22T22:30:49.466Z","etag":null,"topics":["anthropic","claude-code","claude-code-marketplace","claude-code-plugin","claude-plugin","mcp","telegram","telegram-bot","telegram-group-forwarder"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/2naive.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-07T01:52:50.000Z","updated_at":"2026-07-22T21:59:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/2naive/claude-telegram-topics","commit_stats":null,"previous_names":["2naive/claude-telegram-topics"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/2naive/claude-telegram-topics","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2naive%2Fclaude-telegram-topics","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2naive%2Fclaude-telegram-topics/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2naive%2Fclaude-telegram-topics/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2naive%2Fclaude-telegram-topics/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/2naive","download_url":"https://codeload.github.com/2naive/claude-telegram-topics/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2naive%2Fclaude-telegram-topics/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35877012,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-25T02:00:06.922Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anthropic","claude-code","claude-code-marketplace","claude-code-plugin","claude-plugin","mcp","telegram","telegram-bot","telegram-group-forwarder"],"created_at":"2026-07-16T19:00:21.316Z","updated_at":"2026-07-25T10:30:27.584Z","avatar_url":"https://github.com/2naive.png","language":"TypeScript","funding_links":[],"categories":["Source Catalog"],"sub_categories":[],"readme":"# claude-telegram-topics\n\n[![Release](https://img.shields.io/github/v/tag/2naive/claude-telegram-topics?label=release\u0026sort=semver)](https://github.com/2naive/claude-telegram-topics/releases)\n[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)\n[![Claude Code plugin](https://img.shields.io/badge/Claude%20Code-plugin-8A2BE2)](https://code.claude.com/docs/en/plugins)\n[![Tests](https://img.shields.io/badge/tests-passing-brightgreen)](./test)\n\nA **Telegram channel for [Claude Code](https://claude.com/claude-code)** where\n**1 project = 1 forum topic**. Every project's session streams into its own\ntopic inside a single Telegram forum group, and multiple projects run\n**concurrently** — each in its own thread.\n\nInbound messages arrive in your session automatically (as a real Claude Code\nchannel); you reply with a tool. A clean, self-marketplaced fork of Anthropic's\nofficial Telegram plugin (Apache-2.0) — see [`NOTICE`](./NOTICE).\n\n## Why\n\nTelegram allows exactly one `getUpdates` consumer per bot token, so the official\nplugin is single-session and has no per-project threads. This fork fixes both:\n\n- **Project → topic.** The git root of your session's cwd maps to a stable forum\n  topic (persisted in `topics.json`). Reopen the same repo — even from a\n  subdirectory, even in a second session — and you land in the same topic.\n- **Concurrent projects.** The first session to start binds a loopback control\n  port and becomes the **leader**: it owns the single bot poller. Every other\n  session is a **follower** that reaches the bot over `127.0.0.1`. If the leader\n  exits, the next session transparently re-elects.\n- **Status at a glance.** Each topic's name carries a live badge visible in the\n  topic list — ⏳ working · 🟢 ready · 🔔 needs you · 📥 queued · 💤 no session —\n  so you can see which projects Claude is working on and which are idle without\n  opening them.\n- **Drive it from your phone.** Message a dead project and its session\n  **resurrects with the conversation resumed**; launch projects with `/start`;\n  approve tool calls with Allow/Deny buttons — no reaching back to the terminal.\n\n```\n session A ─┐\n session B ─┼─▶ leader (owns bot + poller) ─▶ Telegram forum group\n session C ─┘         ▲                         ├─ topic: project A\n   (followers, HTTP over 127.0.0.1)             ├─ topic: project B\n                                                └─ topic: project C\n```\n\n## Contents\n\n- [Quickstart](#quickstart)\n- [Recovery \u0026 autostart](#recovery--autostart)\n- [Use it](#use-it) · [Liveness](#liveness) · [Approving tool calls](#approving-tool-calls-from-telegram) · [Topics \u0026 sessions](#topics--sessions)\n- Reference: [Launch flags \u0026 the channels allowlist](#launch-flags--the-channels-allowlist) · [Launch alias](#launch-alias) · [Settings](#settings) · [State files](#state-files) · [Token \u0026 group id](#getting-the-bot-token-and-group-id) · [Access control](#access-control)\n- Operations: [Upgrading](#upgrading) · [Troubleshooting](#troubleshooting) · [Uninstall](#uninstall)\n- [Security notes](#security-notes) · [Limitations](#limitations) · [Development](#development)\n\n## Quickstart\n\nYou need ([details](#getting-the-bot-token-and-group-id)):\n\n- [Bun](https://bun.sh) on the `PATH` Claude Code runs in (the bridge runs on\n  it; the first launch runs `bun install` once, so it needs network access).\n- A Telegram **bot** token ([@BotFather](https://t.me/BotFather) → `/newbot`) —\n  use a **dedicated** bot: sharing a token with another integration causes a\n  persistent `409 Conflict`.\n- A **forum-enabled supergroup**: enable **Topics**, add the bot as **admin**\n  with **Manage Topics**.\n\nInstall:\n\n```\n/plugin marketplace add 2naive/claude-telegram-topics\n/plugin install telegram-topics@claude-telegram-topics\n/reload-plugins\n```\n\nThen three steps:\n\n1. **Configure** — writes `~/.claude/channels/telegram-topics/.env` and runs a\n   preflight (token valid, group is a forum, bot can manage topics, token slot\n   free, channel allowlisted), so mistakes surface as verdicts, not silence:\n   ```\n   /telegram-topics:configure \u003cbot-token\u003e\n   /telegram-topics:configure group \u003c-100…group-id\u003e\n   ```\n2. **Allowlist** — approves the channel for `--channels` (one admin prompt;\n   [why this is needed](#launch-flags--the-channels-allowlist)):\n   ```\n   /telegram-topics:allowlist\n   ```\n   No admin rights? Launch with the development flag instead — see\n   [the no-admin alternative](#no-admin-alternative-the-development-flag).\n3. **Launch**:\n   ```\n   claude --permission-mode auto --channels plugin:telegram-topics@claude-telegram-topics\n   ```\n\n**Smoke test:** send a message to this project's topic in the group — it\narrives in the session as a `\u003cchannel\u003e` tag, and Claude's answer lands back in\nthe topic.\n\nWorth doing next: make the launch one word ([Launch alias](#launch-alias)) and\nset `TG_TOPICS_AUTOSTART=1` so dead projects resurrect on a message\n([next section](#recovery--autostart)).\n\nNotes:\n\n- Until the token is configured, the plugin shows as *failed*\n  (`missing TELEGRAM_BOT_TOKEN…`) — that is expected; configure and relaunch.\n- `/reload-plugins` is safe **here** — on first install no channel server is\n  running yet, and reloading is what makes the `/telegram-topics:*` skills\n  available in the current session. After **upgrades** the rule flips:\n  **restart the session, don't reload** — reloading respawns a live bridge in\n  place, with all the failure modes that entails (see\n  [Upgrading](#upgrading)).\n\n## Recovery \u0026 autostart\n\nThe topic outlives the session. Message a project whose session is gone —\nafter a reboot, a crash, a closed console — and:\n\n- the message is **queued** (up to 20 / 30 min) and the topic gets a\n  `📴 No active session` notice with a **▶️ Start session** button;\n- tap it — or set `TG_TOPICS_AUTOSTART=1` in the `.env` to skip the button —\n  and the leader opens a console on the machine (running\n  [`TG_TOPICS_LAUNCH_CMD`](#settings)) that **resumes the project's most recent\n  conversation** (`--continue`);\n- once the session registers, the queued messages are delivered — with\n  confirmation: if a freshly started session doesn't begin a turn within 45 s,\n  delivery is retried (twice), then an honest ⚠️ notice is posted instead of\n  silence.\n\nSo the loop after a reboot is: **message the topic → console opens →\nthe conversation continues where it left off.** No touching the machine.\n\nAlso:\n\n- **`/start`** (any topic, incl. General) — bare `/start` lists bridged\n  projects as buttons; tap one to launch. `/start \u003cpath\u003e` bridges **and**\n  launches a brand-new project — gated by\n  [`TG_TOPICS_LAUNCH_ROOTS`](#settings) (default-deny: launching a path named\n  in a chat message is remote code-exec, so opt in with roots you trust).\n  Brand-new projects start fresh; relaunches resume. The **first-ever** launch\n  of a new folder shows Claude Code's own trust prompt on the machine (one\n  Enter); every later relaunch is hands-off.\n- **`/stop`** (in a project's topic) — end that project's session(s) from the\n  phone: the leader kills their claude process trees (each session reports its\n  pid at registration). A later message or ▶️ relaunches and **resumes** the\n  conversation. Sessions started by pre-0.17.0 clients report no pid and must\n  be closed on the machine.\n- **`/new`** (in a project's topic) — `/stop` + immediately launch a **fresh**\n  session (no `--continue`): the from-the-phone way to clear the context and\n  start over in the same topic.\n- Remote launch is **Windows-only** for now (it opens a real console window,\n  so the session has a TTY and survives).\n\n## Use it\n\n- Messages you send to a project's topic arrive in that project's session\n  automatically as `\u003cchannel source=\"telegram-topics\" …\u003e` tags. If you attach a\n  file, its downloaded local path appears in the message as `saved:\u003cpath\u003e`\n  (kept 24 h — see [State files](#state-files)).\n- The bot only listens inside the configured forum group's **topics**: DMs to\n  the bot and messages in the **General** topic are ignored (General still\n  answers `/status`, `/list` and `/start`), as are messages from users outside\n  the allowlist — silently, by design.\n- The session replies with tools, all scoped to this project's topic:\n\n| Tool | Purpose |\n| --- | --- |\n| `send_message(text, options?)` | Post to this project's topic. Markdown is rendered via explicit entities — snake_case and file_names stay literal, malformed markup degrades to plain text, and messages over 4096 chars are split at line boundaries automatically. Pass `options` (labels) to attach tappable inline buttons — the tap returns as a `[button] \u003clabel\u003e` message, so this doubles as a multiple-choice prompt. |\n| `send_file(path, caption?)` | Send a local file (photo or document). Refuses to send channel state. |\n| `react(message_id, emoji)` | React to a **recent message in the topic — yours included** (the \"seen 👍\" ack). Telegram's fixed reaction set only. (Reacting to *your* messages is tracked in memory by the current leader, so it works until the next leader hand-off/upgrade.) |\n| `edit_message(message_id, text)` | Edit a message the bot sent (most recent ~1000 guaranteed). Keeps the formatting pipeline and re-attaches un-answered choice buttons; editing to identical text is a no-op, not an error. |\n\n- Telegram flood control (`429 retry_after`) and transient API failures are\n  retried automatically with bounded backoff; a real outage surfaces as a tool\n  error instead of hanging.\n\n**Asking a question.** Claude Code's built-in multiple-choice UI (the terminal\nquiz) is *not* bridged to channels — a Telegram-only user never sees it. To ask a\nchoice question remotely, call `send_message` with `options`: each label becomes\nan inline button and the tap arrives back as `[button] \u003clabel\u003e`. (The `/effort`\nlevel is a local TUI control with no remote hook — set it at launch or via\n`effortLevel` in settings.)\n\n## Liveness\n\nYou never have to guess whether the bridge is alive:\n\n- **Status badge** — each topic's **name** is prefixed with a live glyph, the\n  one per-project signal Telegram shows in the topic *list*: **⏳ working**\n  (Claude is processing a turn) · **🟢 ready** (session alive, awaiting you) ·\n  **🔔 needs you** (a permission prompt is waiting) · **📥 queued** (messages\n  held, no session) · **💤 no session** (the CLI isn't running). Working/idle\n  comes from the plugin's activity hooks (`hooks/hooks.json`, auto-active) plus\n  the leader's own signal for Telegram-driven turns; it flips per turn. Set\n  `TG_TOPICS_STATUS_ICONS=0` to keep topic names unbadged.\n- **`/status`** (in any topic, or General) — the leader itself answers with its\n  version, pid and uptime, then **one line per bridged project** (its glyph +\n  name + session labels, or `N queued, no session`, or `no session`) and a\n  legend line — so you see idle and offline projects too, not just live ones.\n  Total silence means the bridge itself is down.\n- **`/list`** (in any topic, or General) — the navigation counterpart: one\n  tappable line per bridged project, **deep-linking straight into its topic**\n  (no scrolling Telegram's topic list), plus a **Launchable** section — unbridged\n  directories found under `TG_TOPICS_LAUNCH_ROOTS`, each with a ready\n  `/start \u003cpath\u003e` command (tap the command to copy it).\n- **Typing indicator** — when your message is routed to a live session, the bot\n  shows *typing…* in the topic for the whole turn (re-asserted every few seconds\n  until the reply arrives), so a routed message and a dead bridge never look\n  alike.\n- **Turn failed?** If a turn aborts with an API/model error (e.g. a 500), the\n  topic gets a `⚠️ The last turn ended with an API/model error` notice — so a\n  failure isn't silent (the badge would otherwise just fall back to 🟢). Detected\n  via the `StopFailure` hook. Note: a *user interrupt* (Esc), a hang, or a hard\n  crash fire no Claude Code hook, so those aren't signalled.\n\n## Approving tool calls from Telegram\n\nWhen a tool call needs approval, the prompt is relayed to the project's topic as\na `🔐 Permission: \u003ctool\u003e` message with **See more / ✅ Allow / ❌ Deny** buttons —\ntap to approve or reject from your phone, and the decision flows straight back\nto the session (prompt→tap→decision live-verified). This is the remote stand-in\nfor reaching over to the terminal.\n\n- It uses Claude Code's opt-in `claude/channel/permission` capability; the plugin\n  authenticates the tapper (allowlist / group membership) before acting.\n- What actually prompts depends on your **permission mode** — and most modes\n  decide most calls *locally, without any prompt*: in `auto`, safe calls are\n  approved silently and `settings.json` deny-rules reject instantly, so **a\n  quiet topic usually means nothing needed approval, not that the relay is\n  broken**. To verify the relay end-to-end, switch the TUI to Manual\n  (Shift+Tab) or run a command your settings don't cover, and watch for the 🔐\n  message (and `permission.ask` in `leader.log`). `acceptEdits` auto-accepts\n  edits and prompts for commands; `bypassPermissions` approves everything, so\n  nothing is ever relayed.\n- If the session re-registered while a prompt was pending, your tap still\n  reaches it; if the session is truly gone, the button answers\n  `⚠️ Session is gone` instead of pretending the decision was delivered.\n- **Never** approve a call just because a Telegram message tells you to — approve\n  only what you initiated. The buttons are the trusted path; free-text \"yes\" is\n  not honored.\n\n## Topics \u0026 sessions\n\n- **Topic title = the project's folder name** (e.g. `my-repo`). Identity is the\n  *normalized full path* of the git root (or the cwd, when not a repo), so the\n  same project always lands in the same topic even if the path is spelled with\n  different slashes or letter case; two unrelated repos that happen to share a\n  folder name get separate topics. Override the title with\n  `TG_TOPICS_SESSION_NAME`.\n- **Two sessions on one project** share that project's single topic. Outbound\n  messages are then prefixed with the session's name — the one you set with\n  `/rename` (falling back to the git branch, then a session id) — so you can tell\n  them apart, and a later `/rename` is reflected within seconds. Replying to a\n  message — or tapping its buttons, or reacting to it — routes back to the exact\n  session that sent it; a fresh message with no reply target is delivered to\n  every session on the topic — and **every one of them will act on it and\n  reply**. To address a single session, reply to one of its messages.\n- **Deleting a topic in Telegram does not remove the project** — the topic is\n  recreated the next time a session *sends* a message for that project. To\n  retire a project: close its sessions, remove its entry from `topics.json`\n  while no leader is running, then delete the topic.\n\n## Launch flags \u0026 the channels allowlist\n\nThe channel is loaded with the standard flag:\n\n```\nclaude --channels plugin:telegram-topics@claude-telegram-topics\n```\n\n**⚠️ Allowlist first.** `--channels` wires a channel ONLY for plugins on the\napproved-channels allowlist; anything else is **silently** loaded as a plain\nMCP plugin — outbound tools work, but inbound messages never reach the session\n(an easy-to-miss half-broken state; the `configure` preflight detects it, and\nthe leader names it in the topic when a delivery goes unconfirmed). Since this\nfork is not on Anthropic's default allowlist, approve it with one command:\n\n```\n/telegram-topics:allowlist\n```\n\nIt writes machine-level **managed settings**\n(`C:\\Program Files\\ClaudeCode\\managed-settings.json` on Windows — one UAC\nprompt; `/Library/Application Support/ClaudeCode/managed-settings.json` on\nmacOS; `/etc/claude-code/managed-settings.json` on Linux — via the printed\n`sudo` command), merge-safe with whatever is already there:\n\n```json\n{\n  \"channelsEnabled\": true,\n  \"allowedChannelPlugins\": [\n    { \"plugin\": \"telegram-topics\", \"marketplace\": \"claude-telegram-topics\" },\n    { \"plugin\": \"telegram\", \"marketplace\": \"claude-plugins-official\" }\n  ]\n}\n```\n\nThe list **replaces** the default Anthropic allowlist — that is why the\nofficial plugin's entry is carried along (keep entries for any other channel\nplugins you use). Sessions read the allowlist at startup, so restart them\nafter the write.\n\n### No-admin alternative: the development flag\n\nWithout machine admin rights, launch with the development-channels flag\ninstead — it bypasses the allowlist entirely:\n\n```\nclaude --permission-mode auto --dangerously-load-development-channels plugin:telegram-topics@claude-telegram-topics\n```\n\nTrade-off: an interactive \"local development\" confirmation gates **every**\nstart — fine for sessions you start by hand, **unusable for hands-off remote\nrelaunch/autostart** (the spawned console hangs at the prompt).\n\n| Symptom | Cause → fix |\n| --- | --- |\n| Send tools work, badge updates, but messages from Telegram never arrive | Not allowlisted under `--channels` → `/telegram-topics:allowlist`, restart the session |\n| Spawned console hangs at \"WARNING: Loading development channels\" | Dev flag in `TG_TOPICS_LAUNCH_CMD` → switch the launch command to `--channels` + allowlist |\n| \"not on the approved channels list\" warning at startup | Allowlist file missing/incomplete at the managed-settings path → `/telegram-topics:allowlist` |\n\nBoth channels flags take the channel list **as their own argument** and consume\neverything after them as channel names. **Put every other option (e.g.\n`--permission-mode auto`) BEFORE the channels flag**, or that option is\nsilently swallowed. (These flags may not appear in `claude --help`; they exist.)\n\n**The bridge itself runs in every session once the plugin is enabled** — flags\nor not: it elects a leader, holds the bot's single `getUpdates` slot, creates\nthis project's topic, and the outbound tools work. What the launch flag adds is\nthe *channel* half: inbound messages arriving as `\u003cchannel\u003e` tags and the\ntool-approval relay. A session launched without the flag drains its copy of\ninbound messages into a queue the model never sees — so for day-to-day use,\nalways launch with the flag; to turn the bridge fully off, disable the plugin\n(`/plugin`), not just the flag.\n\n## Launch alias\n\nWrap the launch in a shell alias so a single word opens a channel-enabled\nsession from any project directory. Note the flag order: mode first, channels\nflag last. (Prefer `--permission-mode auto` — it feeds the\n[approval relay](#approving-tool-calls-from-telegram); use `acceptEdits` for a\nmore conservative mode that prompts on commands.)\n\n**Bash / Zsh** — add to `~/.bashrc` (or `~/.zshrc`), then `source` it:\n\n```\nalias claudet='claude --permission-mode auto --channels plugin:telegram-topics@claude-telegram-topics'\n```\n\n**Windows** — drop a `claudet.cmd` anywhere on your `PATH`:\n\n```\n@claude --permission-mode auto --channels plugin:telegram-topics@claude-telegram-topics %*\n```\n\nOr, for PowerShell, add a function to `$PROFILE`:\n\n```\nfunction claudet { claude --permission-mode auto --channels plugin:telegram-topics@claude-telegram-topics @args }\n```\n\n**No admin rights** (channel not allowlisted)? Same aliases with the\n[development flag](#no-admin-alternative-the-development-flag) — expect its\nconfirmation on every start:\n\n```\nalias claudet='claude --permission-mode auto --dangerously-load-development-channels plugin:telegram-topics@claude-telegram-topics'\n```\n\n`%*` / `@args` still let you append per-run options — but remember they land\n*after* the channels flag, so pass extra options by editing the alias instead.\n\n## Settings\n\nAll optional, set in the `.env` (or the environment):\n\n| Variable | Meaning |\n| --- | --- |\n| `TELEGRAM_ALLOWED_USER_IDS` | Comma-separated numeric user ids allowed to drive sessions. Empty/absent = any group member. Managed by `/telegram-topics:access`; edits apply within ~15 s (no restart). |\n| `TG_TOPICS_PORT` | Loopback control port (default `8787` — also wrangler dev's default; change it if anything else uses 8787). Must be the same for **all** sessions. |\n| `TG_TOPICS_STATE_DIR` | Relocate the whole state dir (default `~/.claude/channels/telegram-topics`). **Environment-only** — it locates the `.env`, so it cannot be set *inside* it; and remote-launched sessions won't inherit it unless it is exported in the machine's environment. |\n| `TG_TOPICS_SESSION_NAME` | Override the topic title for sessions started in this environment. |\n| `TG_TOPICS_LAUNCH_CMD` | Command used by remote session launch (`/start`, autostart). Default: `claude --permission-mode auto --channels plugin:telegram-topics@claude-telegram-topics`. On a **relaunch** of a known project (autostart / Start-session button) `--continue` is inserted (before the variadic channels flag) so the conversation resumes; a new `/start \u003cpath\u003e` starts fresh. If your custom command already selects a conversation (`-c`/`--continue`/`-r`/`--resume`), it's left as-is. The line is passed to `cmd` **verbatim** (Windows): quoted arguments work, and cmd metacharacters (`\u0026`, `|`, `^`) are interpreted by the outer `cmd /c` layer. |\n| `TG_TOPICS_AUTOSTART` | `1` = when a message arrives for a project with no live session, launch one automatically (Windows only) instead of offering a button. |\n| `TG_TOPICS_LAUNCH_ROOTS` | Semicolon-separated trusted directories under which `/start \u003cpath\u003e` may launch a **brand-new** project (one not yet in `topics.json`). **Default-deny**: unset = launch-by-path disabled. Launching an arbitrary path named in a chat message is remote code-exec, so keep this confined to roots you trust (e.g. `C:\\Users\\you\\code`). |\n| `TG_TOPICS_STATUS_ICONS` | Topic-name status badge (see [Liveness](#liveness)). On by default; set `0` to keep topic names unbadged. |\n\n**Environment variables take precedence over the `.env`.** If\n`TELEGRAM_BOT_TOKEN`, `TELEGRAM_GROUP_CHAT_ID` or `TELEGRAM_ALLOWED_USER_IDS`\nare exported in the shell Claude Code starts from, those values silently win\nover whatever `configure` wrote. You can hand-edit the `.env` in any editor;\nthe runtime parser tolerates CRLF line endings.\n\n## State files\n\nEverything lives in `~/.claude/channels/telegram-topics/`:\n\n| File | Purpose | Safe to delete? |\n| --- | --- | --- |\n| `.env` | Bot token, group id, settings | Deleting unconfigures the channel |\n| `topics.json` | project → topic map | Yes, but projects get **new** topics (old ones stay in the group) |\n| `sent.json` | reply/button/reaction routing for recent messages | Yes; replies and taps on older messages stop routing to their exact session |\n| `leader.log`(.1) | leader diagnostics (JSONL, 1 MB rotation) | Yes |\n| `inbox/` | files you attach in Telegram, downloaded locally | Deleted after **24 h** while the bridge is running — copy anything you need to keep. Downloads are capped at 20 MB / 15 s; an oversized or slow attachment arrives as a message with **no** `saved:` path. |\n\n## Getting the bot token and group id\n\n- **Bot token** — create one with [@BotFather](https://t.me/BotFather) (`/newbot`).\n  If you already run the official Telegram plugin, its token lives in\n  `~/.claude/channels/telegram/.env` (`TELEGRAM_BOT_TOKEN`) — but use a\n  **dedicated** bot here: two pollers on one token cause a persistent `409\n  Conflict`. Note the official plugin polls in **every** session while it is\n  merely *enabled* — launch flags are irrelevant. If you migrate its token,\n  first disable it: `/plugin` → disable `telegram@claude-plugins-official` (or\n  set `\"telegram@claude-plugins-official\": false` under `enabledPlugins` in\n  `~/.claude/settings.json`). With a dedicated token this does not apply.\n- **Group id** — the bot must be a member of the group, and you must enable\n  **Topics** first (that turns the group into a supergroup and finalizes its id).\n  Then either add a helper bot such as [@getidsbot](https://t.me/getidsbot) or\n  `@RawDataBot` to the group, or send a message in it and read the id back from\n  the Bot API:\n  ```\n  curl \"https://api.telegram.org/bot\u003ctoken\u003e/getUpdates\"\n  ```\n  Use the `message.chat.id` value — a forum supergroup id starts with `-100`.\n\n## Access control\n\nBy default any member of the forum group can drive sessions **and approve\nrelayed tool calls** (the group's membership is the boundary). To restrict to\nspecific users:\n\n```\n/telegram-topics:access allow \u003cnumeric-user-id\u003e\n/telegram-topics:access            # show the current allowlist\n```\n\nAllowlist edits — including revocations — are re-read by the running leader\nwithin ~15 seconds; no restart needed. (A shell-exported\n`TELEGRAM_ALLOWED_USER_IDS` pins the list for the process lifetime instead.)\n\n## Upgrading\n\nA plugin update only takes effect in **new** sessions — and the **leader** (the\nprocess that owns the bot poller) may be an old session still running the\nprevious version.\n\nFrom **0.6.0** this resolves itself: sessions announce their version when they\nregister, and a leader that hears from a strictly newer session gracefully\nsteps down (bot poller first, then the port) while the newer session claims\nleadership. Update the plugin, start one new session, and the fleet is on the\nnew code — no manual process hunting.\n\nThe hand-off needs both sides to speak it, so upgrading **from 0.5.x or\nolder** still requires the old ritual once: close **all** running channel\nsessions (or kill the leader process), then relaunch — the first new session\ntakes leadership on the new code.\n\n**After an upgrade, restart the session — don't `/reload-plugins`.** Reloading\nplugins inside a live session respawns this server in place: the replacement\nmay briefly miss its session record (0.7.0 rides that out by waiting, then\nself-healing its registration), and the abandoned process must notice on its\nown that it was dropped (0.7.0 adds watchdog teardown for exactly this). Both\nrecover, but a plain session restart is the clean path.\n\n## Troubleshooting\n\nFirst stop: `/status` in the Telegram group, then\n`curl 127.0.0.1:8787/health`, then the leader log\n`~/.claude/channels/telegram-topics/leader.log` (JSONL: registrations,\nhand-offs, routing decisions, drop reasons).\n\n| Symptom | Check |\n| --- | --- |\n| Channel never appears in a session | `bun --version` (on PATH?); `.env` configured? First run needs network for `bun install`. MCP errors: `claude --debug`. |\n| Send tools work, but messages from Telegram **never** arrive | Channel not allowlisted under `--channels` → `/telegram-topics:allowlist`, restart the session ([details](#launch-flags--the-channels-allowlist)). |\n| Messages from Telegram **stop** arriving, replies still go out | `leader.log` → `poller.died` with `409 Conflict` = another poller on this token (official plugin still **enabled**? claudet running elsewhere?). Re-election backs off 60 s on 409/401, so fix the cause, don't just restart. |\n| `registration failed: HTTP …` / port errors | Something else on port 8787 (wrangler dev?) — the client now names this; set `TG_TOPICS_PORT` in the `.env` for all sessions. |\n| Message posted, nothing happens, no typing | No live session → look for the `📴` notice / tap **▶️ Start session** / `/status`. Posted in General or as a DM? Those are ignored by design. Sender not on the allowlist? |\n| Buttons answer \"Request no longer available\" | The leader restarted after the prompt was posted — re-run the tool call; the fresh prompt relays again. |\n| Token / group errors at setup | `/telegram-topics:configure` re-runs preflight with per-check verdicts (token, group reachable, forum, admin rights, free token, allowlist). |\n\n## Uninstall\n\n1. Close all channel sessions (the leader dies with the last one).\n2. `/plugin uninstall telegram-topics@claude-telegram-topics`.\n3. Delete `~/.claude/channels/telegram-topics/` — **it contains the bot token**\n   (`.env`), plus the topic map and downloaded attachments. Plugin removal does\n   not touch it.\n4. Optionally revoke the token in [@BotFather](https://t.me/BotFather)\n   (`/revoke`), delete the forum topics, and remove the plugin's entry from the\n   managed-settings allowlist.\n\n## Security notes\n\n- The bot token only ever reaches `api.telegram.org`. The control API binds to\n  `127.0.0.1` only.\n- The loopback control API is **unauthenticated** — any local process can reach\n  it. Fine for a single-user machine; do not run on shared/multi-user hosts as-is.\n- `send_file` refuses paths inside the state dir (won't leak the token).\n- Remote session launch (`/start`, the ▶️ button, autostart) executes\n  `TG_TOPICS_LAUNCH_CMD` on your machine. Launch targets are a project already in\n  `topics.json` **or**, via `/start \u003cpath\u003e`, any directory under a\n  `TG_TOPICS_LAUNCH_ROOTS` trusted root (default-deny, `..`-traversal-proof) —\n  so treat allowlist membership as \"may start Claude Code at any path under the\n  trusted roots\". Note the **default empty allowlist is fail-open**,\n  so on a shared group this means any member can launch sessions too, not just\n  send messages — set an allowlist if that matters.\n- Use a **dedicated bot token** for this plugin. Reusing the same token as\n  another running Telegram integration (e.g. the official plugin — which polls\n  whenever it is *enabled*, in every session) makes two pollers fight over\n  `getUpdates` — Telegram returns persistent 409 Conflict.\n- The tool-approval relay trusts whoever can tap a button in the topic. On a\n  private group that is just you; if the group has other members, restrict\n  approvals with an explicit allowlist (`/telegram-topics:access allow \u003cid\u003e`).\n- The managed-settings allowlist is machine policy: it applies to every Claude\n  Code session on the machine and needs admin to change — that is the point.\n- Uninstalling the plugin does **not** delete the stored token — see Uninstall.\n\n## Limitations\n\n- A not-yet-answered `🔐 Permission` prompt dies with its leader (the tap then\n  answers \"Request no longer available\" — honest, but the prompt must be\n  re-triggered). Re-election after a leader death is bounded by one poll cycle\n  (~30 s); messages sent to a session-less project are queued for 30 min rather\n  than lost.\n- One forum group per machine (all projects share it, one topic each).\n- Remote session launch is Windows-only.\n- A **user interrupt** (Esc), a hang, or a hard crash fire no Claude Code hook,\n  so — unlike an API error, which raises a `⚠️` notice via `StopFailure` — those\n  cases aren't signalled to Telegram; the working badge falls back to 🟢 on a\n  15-minute TTL backstop.\n- The first message delivered to a freshly auto-started session can take up to\n  ~45 s to surface (the delivery-confirmation retry window); messages to a\n  running session are immediate.\n\n## Development\n\n```\nbun install\nbun run typecheck\nbun test\n```\n\nTests are hermetic: `bunfig.toml` preloads `test/preload.ts`, which pins the\nstate dir, port and credentials to throwaway values so the suite can never\ntouch a live channel.\n\n## License\n\n[Apache-2.0](./LICENSE). Derivative work — see [`NOTICE`](./NOTICE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F2naive%2Fclaude-telegram-topics","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F2naive%2Fclaude-telegram-topics","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F2naive%2Fclaude-telegram-topics/lists"}