A Personal Claude Code Agent on Telegram
4 min read757 words

A Personal Claude Code Agent on Telegram

Guide
Technology
AI
Productivity

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

claude
CLI on my VPS. A couple hundred lines of Python I understand end to end beats a framework I do not. This post walks the pieces worth stealing, not a clone-and-run repo.

Prerequisites

  • A VPS or home server with the
    claude
    CLI installed and logged in (run
    claude
    once, 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

CLAUDE.md
, my skills, and my MCP servers. There is nothing to re-wire. The tradeoff is real (no native streaming, no built-in media handling), but for a text chat agent it is a fair trade for zero extra moving parts.
--dangerously-skip-permissions
is what makes it unattended: the agent reads, writes, and runs commands without a confirmation prompt, so guard it behind an allowlist (below) and a workspace you are comfortable handing over.

2. Sessions that survive across messages

claude -p
is one-shot, but it returns a
session_id
, and it will resume any session you hand back with
--resume
. That is the entire trick to making a stateless CLI feel like a chat. Store one id per Telegram chat:

# 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

0600
, it holds your session handles) and the conversation continues across restarts. A
/new
command just drops the id for that chat and the next message starts fresh.

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

/stop
cancels the running asyncio task,
/status
reports running or idle, and every real Claude slash command (
/model
, custom skills, whatever) passes straight through to the CLI. Claude replies in Markdown, which you render to Telegram HTML with a fallback to plain text when a message will not parse.

4. Bonus: another model through Claude Code

Claude Code speaks to any Anthropic-compatible endpoint, so a

/provider
command can point the same subprocess at a different backend by setting environment variables for that run:

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

/status
to confirm it is alive and
/new
to wipe the slate. That is the whole bridge: a couple hundred lines of Python and one subprocess call.

A Personal Claude Code Agent on Telegram