
A Personal Claude Code Agent on Telegram
Full options already exist for this: Nanobot, Openclaw, Linuz90's SDK bot with voice and images. I passed on all of them for one reason. I like knowing exactly what I run, and my needs are small: a text agent on my phone that talks to the
Prerequisites
- A VPS or home server with the claudeCLI installed and logged in (runclaudeonce, interactively, to authenticate).
- A Telegram bot token from @BotFather and your numeric Telegram user ID.
- Python 3.12 and python-telegram-bot.
1. The runner: a subprocess, not the SDK
The whole bridge rests on one call. Instead of the Agent SDK, I shell out to the CLI in headless mode and ask for JSON back:
cmd = [claude_bin, "-p"] if session_id: cmd += ["--resume", session_id] cmd += ["--output-format", "json", "--dangerously-skip-permissions"] proc = await asyncio.create_subprocess_exec( *cmd, cwd=workspace, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, _ = await proc.communicate(prompt.encode()) data = json.loads(stdout) result_text, session_id = data["result"], data["session_id"]
Why a subprocess? Because the CLI already carries my login, my
2. Sessions that survive across messages
# after each turn sessions[chat_id] = result.session_id # persist to a small JSON file # on the next turn session_id = sessions.get(chat_id) # passed back via --resume
Persist that map to a JSON file (mode
3. Talking to it
One message handler, gated by a user allowlist, forwards everything to Claude except a handful of bridge commands it handles itself:
if user.id not in ALLOWED_USER_IDS: return # silent drop for anyone else if text == "/new": return await bridge.new(chat_id) if text == "/stop": return await bridge.stop(chat_id) if text == "/status": return bridge.status(chat_id) # anything else -> claude
4. Bonus: another model through Claude Code
Claude Code speaks to any Anthropic-compatible endpoint, so a
DEEPSEEK_ENV = { "ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic", "ANTHROPIC_AUTH_TOKEN": DEEPSEEK_API_KEY, "ANTHROPIC_MODEL": "deepseek-v4-pro", }
Switching provider cancels the running turn and starts a fresh session for that chat. The rest of the bridge does not change; DeepSeek runs through the exact same Claude Code machinery, tools and all.
5. Running it with systemd
On the VPS the bridge runs as a dedicated unprivileged user with a systemd user unit and lingering enabled, so it starts on boot without a login session:
# as the claude user loginctl enable-linger claude systemctl --user enable --now claude-bridge
# ~/.config/systemd/user/claude-bridge.service [Unit] Description=Claude Telegram bridge After=network.target [Service] ExecStart=%h/.local/bin/uv run claude-bridge Restart=always RestartSec=5 EnvironmentFile=%h/.config/claude-bridge/env [Install] WantedBy=default.target
Hello World
Message the bot from Telegram. The first message opens a session; the next one continues it. Send