5.1 KiB
Telegram bot
hosts/Volcanic.AgenticCore.Telegram lets you chat with the agent from Telegram. It uses long polling (no public
URL, no webhook), gives every private chat its own agent session and workspace, and runs the model through the
Claude Code provider (claude-code/claude-opus-5-5) on your Claude subscription.
Setup
- Create a bot with @BotFather (
/newbot) and copy the token. - Find your numeric Telegram user id, for example by messaging @userinfobot.
- Install Claude Code on the machine that runs the bot and log in (
claude, then/login). Check withclaude --version; see claude-code-provider.md. - Run:
TELEGRAM_BOT_TOKEN=123456:ABC... \
TELEGRAM_ALLOWED_USER_IDS=111111111 \
dotnet run --project hosts/Volcanic.AgenticCore.Telegram
Open the bot in Telegram and say hello.
Configuration
Environment variables, or the Telegram section of appsettings.json (Telegram__Name in the environment):
| Setting | Default | Notes |
|---|---|---|
TELEGRAM_BOT_TOKEN / Telegram:BotToken |
— | Required |
TELEGRAM_ALLOWED_USER_IDS / Telegram:AllowedUserIds |
— | Required, comma separated. The bot refuses to start without it |
Telegram:WorkspaceRoot |
~/.volcanic/telegram-workspaces |
Chat <id> works in <root>/<id>; put AGENTS.md, PERSONA.md, MEMORY.md there |
Telegram:Model |
claude-code/claude-opus-5-5 |
Any provider/model the Claude Code provider serves |
Telegram:EditInterval |
00:00:01.5 |
Minimum time between edits of the "Working..." status message |
Telegram:ReconnectInitialDelay / ReconnectMaxDelay |
1 s / 60 s |
Backoff after lost connection |
Sessions are stored as usual under ~/.volcanic/agent/sessions/telegram-<chatId>/; after a restart each chat resumes
its most recent conversation.
Using it
Write normally. While the agent works, a status message stays at the bottom of the chat:
⚙️ Working...
✓ read: src/Program.cs
✗ bash: dotnet test
▶ bash: dotnet build
It lists the last five tool calls (▶ running, ✓ done, ✗ failed) and is deleted when the run finishes.
Answers are not streamed: each one is sent complete when the model finishes it (split into several messages past
Telegram's 4096 character limit).
A message sent while the agent works steers the current run. Photos (with or without a caption) are passed to the model.
| Command | |
|---|---|
/new |
Start a new conversation |
/stop |
Stop the current run |
/status |
Model, thinking level, session, context usage, workspace |
/sessions, /resume <id> |
List and resume earlier conversations (an id prefix is enough) |
/compact |
Summarise the conversation to free context |
/thinking [level] |
Show or set the thinking level (Claude Code --effort) |
/help |
Short help |
Any other /command is passed to the session, so extension commands such as /claude-code-doctor and
/claude-code-login work.
When an extension asks something, the question appears with buttons (or as a reply prompt for free text).
If the Claude subscription session behind the bot expires, the first id in AllowedUserIds (the bot owner)
gets the re-login link and code prompt in their own private chat, regardless of which chat's request triggered
it. That user must already have started a chat with the bot — Telegram bots cannot message a user first — or
the login goes unanswered and the triggering request fails ("no code was entered"). The link, the steps and the
code prompt arrive as one message. While that prompt is pending, the owner's next plain text message in that chat
is taken as the code and is not sent to the agent; photos and /commands still behave normally. Don't ask the bot
something else in the owner chat until the code is in: a second free-text question in that chat (for example from
an extension) replaces the pending prompt and cancels the login. See
claude-code-provider.md.
Security
The agent has the built-in tools, including bash, and runs them as the bot's OS user. Only allow-listed user ids
are served (everyone else is ignored and logged), and only in private chats. Run the bot as a dedicated user or in a
container, and treat the workspace root as writable by anyone on the allow-list.
Connection loss
Polling uses Telegram.Bot's built-in loop with a hardened error handler:
- a failed
getUpdateswaits 1 s, 2 s, 4 s … up to 60 s (±20 % jitter) before the next attempt, instead of retrying in a tight loop; the first successful poll resets the delay and logsTelegram connection restored; - the update offset only moves past updates that were handed on, so reconnecting neither loses nor repeats messages;
- if the loop itself ever ends while the host runs, it is restarted from the last offset;
- a
401(revoked or wrong token) is the one error it does not retry: the host logs it and exits with code 1.
A reply that fails to send (Telegram unreachable mid-run) is retried after 1 s and 3 s, then logged as lost; the render loop keeps going either way.