* feat(telegram,discord): compact permission messages with expandable details
Replace verbose permission request messages with a compact format showing
only the tool name. Adds a "See more" button that expands inline to show
tool_name, description, and pretty-printed input_preview JSON. Yes/No
buttons replace Allow/Deny. Bump plugin versions to 0.0.4.
* revert: restore Allow/Deny button labels
Replace "Reply 'yes abcde' to allow" text instruction with native
inline buttons (Telegram InlineKeyboard, Discord ButtonBuilder).
One tap to approve/deny instead of typing a 5-char ID.
- Telegram: callback_query handler with allowFrom gate, edits message
to show outcome and remove buttons after decision
- Discord: interactionCreate handler with allowFrom gate, updates
interaction with outcome and clears components
- Text-reply path (PERMISSION_REPLY_RE) kept as fallback
- Bump both plugins to v0.0.3
🏠 Remote-Dev: homespace
Complete the plugin side of anthropics/claude-cli-internal#23061 (permission
prompts over channels).
Capability: both servers now declare
experimental["claude/channel/permission"]
which tells CC they can relay permission requests. This capability asserts the
server authenticates the replier — gate()/access.allowFrom filters
non-allowlisted senders before handleInbound runs.
Outbound (CC → user): setNotificationHandler for
notifications/claude/channel/permission_request
formats the tool name, description, and input preview into a human-readable
message and sends it to every allowlisted DM. Groups are excluded — the
security thread resolution was "single-user mode for official plugins."
Inbound (user → CC): PERMISSION_REPLY_RE intercept in handleInbound catches
"yes xxxxx" / "no xxxxx" replies, emits the structured
notifications/claude/channel/permission
event with {request_id, behavior}, reacts with checkmark/cross, and returns
without relaying the text to Claude as a chat message.
The regex is inlined from channelPermissions.ts (no cross-repo dep). IDs are
lowercased at the plugin boundary per the case-insensitive spec.
Version bumped 0.0.1 → 0.0.2 so the plugin reconciler picks up the change.
🏠 Remote-Dev: homespace
- safeName() strips <>[]\r\n; from file_name/title before they hit the
<channel> notification — delimiter chars would let an uploader break
out of the tag or forge meta entries
- download_attachment strips ext/uniqueId to alphanumeric before join()
— defense-in-depth against path traversal (file_unique_id is
Telegram-controlled so this is belt-and-braces)
- /status in a group would leak the sender's pending pairing code to
other group members, who could then pair as that user
- Commands in non-allowlisted groups confirm bot presence and enable spam
- /start now acknowledges dmPolicy === 'disabled' instead of lying
- setMyCommands scoped to private chats so the / menu only shows in DMs
Message edits don't trigger push notifications on the user's device.
Update system instructions and edit_message tool description to steer
the assistant toward edit-for-progress + new-reply-on-completion.
Fixes#786
Hardcoded ~/.claude/channels/<name>/ meant only one bot per machine.
Respect TELEGRAM_STATE_DIR / DISCORD_STATE_DIR so users can run
multiple bots with separate tokens and allowlists.
Also fixed README path ('in your project' -> '~/...') to match the code.
Fixes#792
During /mcp reload or when a zombie from a previous session still holds
the polling slot, the new process gets 409 Conflict on its first
getUpdates and dies immediately. Retry with backoff until the slot
frees — typically within a second or two.
Also handles the two-sessions case: the second Claude Code instance
keeps retrying (with a clear message about what's happening) and takes
over when the first one exits.
Fixes#804#794, partial #788 (issue 4)
When the MCP stdio transport closes, the bot kept polling Telegram as
a zombie process — holding the token and causing 409 Conflict for the
next session.
- Listen for stdin end/close and SIGTERM/SIGINT -> bot.stop() + exit
- Force-exit after 2s if bot.stop() stalls on the long-poll timeout
- unref the approval-check interval so it doesn't keep us alive
Fixes#793, partial #788 (issue 3)
The bot would silently stop delivering messages after the first error:
grammy's default handler calls bot.stop() on any middleware throw, and
void bot.start() / void mcp.notification() swallow rejections with no log.
- bot.catch(): log and keep polling on handler errors
- bot.start().catch(): log when polling dies (bad token, 409, network)
- mcp.notification().catch(): log when inbound delivery to Claude fails
- process-level unhandledRejection/uncaughtException as a safety net
Fixes#756#759#761#777#809, partial #788
The bot token is a credential. Tighten perms on load so hand-written
or pre-existing .env files get locked down, and update the configure
skill to chmod after writing. No-op on Windows.
Remove the three chat bridge plugins from external_plugins/ and their
corresponding entries in marketplace.json.
Co-authored-by: Claude <noreply@anthropic.com>
Telegram messaging bridge for Claude Code. Runs a local MCP server that
connects to the Telegram Bot API via a user-created bot token.
Built-in access control: inbound messages are gated by an allowlist
(default: pairing mode), outbound sends are scoped to the same allowlist.
The /telegram:access skill manages pairing, allowlists, and policy.
Ships full source — server.ts runs locally via bun, started by the
.mcp.json command. First external_plugins entry to bundle source rather
than point at a hosted MCP endpoint.