Talk to Claude Code from Telegram.
A single-binary Telegram bridge for Claude Code. Spawns claude -p per inbound message, streams the response back as chat messages, survives crashes, works on your phone.
git clone https://github.com/gmcdonald44/hal-telegram
cd hal-telegram
bun install
cp .env.example .env # fill in TELEGRAM_BOT_TOKEN + TELEGRAM_OWNER_CHAT_ID
bun run botThat's it. Message your bot. It replies with Claude Code.
Claude Code is a power tool, but it's tethered to your terminal. If you're coming from a persistent-agent framework (OpenClaw, custom harnesses, Telegram bot duct tape) and you want Claude Code to be your everyday assistant — not just a coding sidekick — you need it reachable from your phone.
This repo is the glue: Telegram → claude -p → Telegram.
- Your messages become Claude Code prompts.
- Each chat gets a persistent session — resume-forever, so Claude remembers what you've been working on.
- Photos and documents are saved locally and handed to Claude with the file path.
- A per-channel queue serializes spawns so sessions don't collide.
/stopkills the active spawn mid-flight.- Single-owner mode by default — only your Telegram user ID can talk to it.
It is explicitly not a framework. Four TypeScript files, no database, no web UI.
| Feature | Detail |
|---|---|
| Text messages | Forwarded as Claude Code prompts |
| Photos | Downloaded to a temp dir, Claude told the path + caption |
| Documents | Same — Claude gets the path, MIME type, size |
| Replies | Original message context prepended ([Replying to X: "..."]) |
| Long responses | Split at 4 KB, sent as multiple Telegram messages |
| Progress updates | Single editable message streams the response live (throttled ~1s); shows 🔧 <tool> during tool calls and drops on completion |
| Abnormal exit | Sends ⚠️ Run exited with code <N> if claude -p crashes so partial output isn't silently "done" |
/stop |
SIGTERMs the running claude -p spawn |
/stop all |
SIGTERM + clear the rest of the queue |
/queue or /status |
Show what's running and what's pending |
/rotate |
Reset the session — next message starts a fresh Claude Code conversation |
- No group chats. It's a DM bridge. If you want to use a bot in a group, you'll need to add
@mentiongating yourself. - No memory kit. Bring your own
CLAUDE.md/ hooks — we don't impose a memory layout. - No multi-user billing / rate limits. Single-owner mode is the intended shape.
- No webhooks. It uses
getUpdateslong polling, which is fine for a personal bot and terrible for 1000s of users.
If you want a persistent-agent layer on top of this (memory files, CLAUDE.md conventions, hooks), build that separately or bring an existing one. This repo deliberately stays out of that opinion space.
Telegram ──getUpdates──▶ bot/index.ts ──enqueue──▶ bot/queue.ts
│ │
│ ▼
│ bot/spawn.ts
│ │
▼ ▼
stream progress ┌──▶ claude -p --resume <session-id>
(throttled ~1s) │ │
▲ │ ▼
└── stream-json events ──────┘
│
▼
sendReply → Telegram
bot/index.ts— grammy handlers (text, photo, document,/stop,/queue,/rotate), progress trackerbot/spawn.ts— thin wrapper aroundclaude -p --output-format stream-json, parses NDJSONbot/channels.ts— JSON registry of chat ID → session ID, persisted to diskbot/queue.ts— per-channel serial queue with SIGTERM-on-stop
Four files. ~800 lines total. Read them.
- Bun — runs the TypeScript directly.
curl -fsSL https://bun.sh/install | bash - Claude Code CLI on your
PATH—claude --versionshould work - An Anthropic API key OR a Claude subscription that lets
claude -prun - A Telegram bot token (free, 2 minutes to get — see below)
Free, no phone verification beyond your existing Telegram account, ~2 minutes.
- Open Telegram and message @BotFather.
- Send
/newbot— follow the prompts:- Display name: whatever you want (shown as the bot's name in chats)
- Username: must be unique across Telegram and must end in
bot(e.g.my_claude_code_bot)
- BotFather replies with an HTTP API token that looks like
1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. This is yourTELEGRAM_BOT_TOKEN. Treat it like a password — anyone with it can impersonate your bot. - Get your numeric user ID for single-owner mode: message @userinfobot → tap start → copy the ID it replies with. This is your
TELEGRAM_OWNER_CHAT_ID. - (Optional polish) Back in @BotFather, register the slash commands so Telegram shows autocomplete when users type
/:- Send
/setcommands→ pick your bot → paste:stop - Kill the active Claude Code spawn queue - Show what's running and what's pending rotate - Start a fresh Claude Code session
- Send
- (Optional)
/setdescriptionand/setabouttextlet you set the text that appears on your bot's profile page.
If you later want to use the bot in a group chat, also send /setprivacy → pick your bot → Disable, so the bot can see all messages (not just ones addressed to it).
Losing the token:
/tokenin @BotFather will reveal it again./revokerotates it — the old token stops working immediately.
See docs/SETUP.md for the full 5-minute walkthrough: filling in .env, first message, always-on runners (systemd / launchd), pointing Claude at a different codebase.
Hitting errors? See docs/TROUBLESHOOTING.md.
All via environment variables (see .env.example):
| Var | Required | Default | Purpose |
|---|---|---|---|
TELEGRAM_BOT_TOKEN |
yes | — | From @BotFather |
TELEGRAM_OWNER_CHAT_ID |
recommended | — | Your Telegram user ID. Lock down single-owner mode. |
CLAUDE_MODEL |
no | claude-opus-4-7 |
Any model your subscription supports |
BOT_NAME |
no | claude-tg-bot |
Prefix for log lines |
CLAUDE_SETTINGS_PATH |
no | .claude/settings.json |
Optional Claude Code settings file |
TMP_DIR |
no | OS tmpdir | Where photos/documents land |
BOT_MODE |
no | prod |
prod or sandbox — separate registries for a test bot |
- The bot runs
claude -p --dangerously-skip-permissionsbecause there's no interactive stdin. Claude Code can read, write, and execute in its working directory. Treat the bot's working directory as untrusted territory — don't run it against~or anywhere sensitive. TELEGRAM_OWNER_CHAT_IDis the only thing between a stranger who guesses your bot handle and a shell on your machine. Set it.CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1is passed to every spawn, which strips Anthropic credentials from the child env. You still need to be careful about other secrets in your env.bot/channel-registry-*.jsoncontains your chat IDs..gitignorecovers them, but don't paste them elsewhere.
MIT — see LICENSE.
Contributions welcome. See docs/CONTRIBUTING.md.