From 521f858e112d7e4e0854abe08d5a34631509d475 Mon Sep 17 00:00:00 2001 From: Kenneth Lien Date: Fri, 20 Mar 2026 11:47:39 -0700 Subject: [PATCH 1/3] telegram: add /start /help /status bot commands --- external_plugins/telegram/server.ts | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/external_plugins/telegram/server.ts b/external_plugins/telegram/server.ts index 8acd52a..e6c8259 100644 --- a/external_plugins/telegram/server.ts +++ b/external_plugins/telegram/server.ts @@ -507,6 +507,52 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => { await mcp.connect(new StdioServerTransport()) +bot.command('start', async ctx => { + await ctx.reply( + `👋 Hi! I'm a bridge between Telegram and Claude Code.\n\n` + + `How to set up:\n` + + `1. Send me any message here\n` + + `2. I'll give you a pairing code\n` + + `3. Run that code in Claude Code to link your account\n\n` + + `Once paired, your messages here go straight to Claude.` + ) +}) + +bot.command('help', async ctx => { + await ctx.reply( + `I relay messages between Telegram and Claude Code.\n\n` + + `What works:\n` + + `- Text messages\n` + + `- Photos (with captions)\n` + + `- Replies to specific messages\n\n` + + `Use /start for setup instructions.` + ) +}) + +bot.command('status', async ctx => { + const from = ctx.from + if (!from) return + const senderId = String(from.id) + const access = loadAccess() + + if (access.allowFrom.includes(senderId)) { + const name = from.username ? `@${from.username}` : senderId + await ctx.reply(`Paired as ${name}.`) + return + } + + for (const [code, p] of Object.entries(access.pending)) { + if (p.senderId === senderId) { + await ctx.reply( + `Pending pairing — run in Claude Code:\n\n/telegram:access pair ${code}` + ) + return + } + } + + await ctx.reply(`Not paired. Send me a message to get a pairing code.`) +}) + bot.on('message:text', async ctx => { await handleInbound(ctx, ctx.message.text, undefined) }) @@ -597,5 +643,10 @@ void bot.start({ onStart: info => { botUsername = info.username process.stderr.write(`telegram channel: polling as @${info.username}\n`) + void bot.api.setMyCommands([ + { command: 'start', description: 'Welcome and setup guide' }, + { command: 'help', description: 'What this bot can do' }, + { command: 'status', description: 'Check your pairing status' }, + ]).catch(() => {}) }, }) From 9a101ba34c8d58410beaaad7f053ba9434fc6953 Mon Sep 17 00:00:00 2001 From: Kenneth Lien Date: Fri, 20 Mar 2026 11:54:48 -0700 Subject: [PATCH 2/3] Restrict bot commands to DMs (security) - /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 --- external_plugins/telegram/server.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/external_plugins/telegram/server.ts b/external_plugins/telegram/server.ts index e6c8259..58ef37b 100644 --- a/external_plugins/telegram/server.ts +++ b/external_plugins/telegram/server.ts @@ -507,7 +507,18 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => { await mcp.connect(new StdioServerTransport()) +// Commands are DM-only. Responding in groups would: (1) leak pairing codes via +// /status to other group members, (2) confirm bot presence in non-allowlisted +// groups, (3) spam channels the operator never approved. Silent drop matches +// the gate's behavior for unrecognized groups. + bot.command('start', async ctx => { + if (ctx.chat?.type !== 'private') return + const access = loadAccess() + if (access.dmPolicy === 'disabled') { + await ctx.reply(`This bot isn't accepting new connections.`) + return + } await ctx.reply( `👋 Hi! I'm a bridge between Telegram and Claude Code.\n\n` + `How to set up:\n` + @@ -519,6 +530,7 @@ bot.command('start', async ctx => { }) bot.command('help', async ctx => { + if (ctx.chat?.type !== 'private') return await ctx.reply( `I relay messages between Telegram and Claude Code.\n\n` + `What works:\n` + @@ -530,6 +542,7 @@ bot.command('help', async ctx => { }) bot.command('status', async ctx => { + if (ctx.chat?.type !== 'private') return const from = ctx.from if (!from) return const senderId = String(from.id) @@ -643,10 +656,13 @@ void bot.start({ onStart: info => { botUsername = info.username process.stderr.write(`telegram channel: polling as @${info.username}\n`) - void bot.api.setMyCommands([ - { command: 'start', description: 'Welcome and setup guide' }, - { command: 'help', description: 'What this bot can do' }, - { command: 'status', description: 'Check your pairing status' }, - ]).catch(() => {}) + void bot.api.setMyCommands( + [ + { command: 'start', description: 'Welcome and setup guide' }, + { command: 'help', description: 'What this bot can do' }, + { command: 'status', description: 'Check your pairing status' }, + ], + { scope: { type: 'all_private_chats' } }, + ).catch(() => {}) }, }) From ea382ec6a43f18478df6acb8b7026fae72eda02a Mon Sep 17 00:00:00 2001 From: Kenneth Lien Date: Fri, 20 Mar 2026 11:55:56 -0700 Subject: [PATCH 3/3] Tighten /start and /help copy Less chatty, more precise. Explicitly mentions the /telegram:access skill and the 6-char code format. --- external_plugins/telegram/server.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/external_plugins/telegram/server.ts b/external_plugins/telegram/server.ts index 58ef37b..38a10ec 100644 --- a/external_plugins/telegram/server.ts +++ b/external_plugins/telegram/server.ts @@ -520,24 +520,21 @@ bot.command('start', async ctx => { return } await ctx.reply( - `👋 Hi! I'm a bridge between Telegram and Claude Code.\n\n` + - `How to set up:\n` + - `1. Send me any message here\n` + - `2. I'll give you a pairing code\n` + - `3. Run that code in Claude Code to link your account\n\n` + - `Once paired, your messages here go straight to Claude.` + `This bot bridges Telegram to a Claude Code session.\n\n` + + `To pair:\n` + + `1. DM me anything — you'll get a 6-char code\n` + + `2. In Claude Code: /telegram:access pair \n\n` + + `After that, DMs here reach that session.` ) }) bot.command('help', async ctx => { if (ctx.chat?.type !== 'private') return await ctx.reply( - `I relay messages between Telegram and Claude Code.\n\n` + - `What works:\n` + - `- Text messages\n` + - `- Photos (with captions)\n` + - `- Replies to specific messages\n\n` + - `Use /start for setup instructions.` + `Messages you send here route to a paired Claude Code session. ` + + `Text and photos are forwarded; replies and reactions come back.\n\n` + + `/start — pairing instructions\n` + + `/status — check your pairing state` ) })