How I Built a Telegram Bot to Run Claude Code From My Phone
I'm constantly coming up with ideas during my commute, and I wanted to push them into the system I've built in real time. So I set up a single Telegram bot that lets me call Claude Code on my Mac from my phone — and that one-line interface has come to mean a lot to me.
01. I wanted to push ideas into my system in real time, from my phone
I run dozens of automation jobs every day. AI ecosystem monitoring, English learning, building a stock-market monitoring AI LLM wiki, brainstorming, and more — I wanted to be able to check all of that from my phone too. Opening the laptop, launching a terminal, and typing commands was just too much hassle during my commute.
It started as a simple notification bot. The automation would send me one line at a time over Telegram. But just receiving notifications got frustrating. I kept thinking, "I wish I could send a message on Telegram and have it handled right away." So I turned the one-way notification bot into a two-way one.
02. The setup: I made the Telegram bot call the Claude CLI directly
I took one sentence — "A Telegram message becomes Claude CLI input as-is, and Claude's response becomes the Telegram reply as-is." — and moved it straight into code.
The structure is three chunks. Bot daemon / Claude CLI caller / safety net.
The bot daemon waits 25 seconds at a time for new messages via Telegram long-poll, and when a message arrives it spins up the Claude CLI as a subprocess, gets the result, and sends it back to Telegram as a reply.
I nailed the bot down as a launchd KeepAlive daemon. Even if it crashes it restarts within 30 seconds, and it comes up automatically on boot.
# schedules/telegram-bot.job (launchd skeleton)
NAME=telegram-bot
RUN_AT_LOAD=1
KEEP_ALIVE=1
COMMAND=/Users/[user]/automation-scripts/telegram-bot.sh
WORKDIR=/Users/[user]/Documents/Claude/Projects/automation
# launchd checks it's alive every cycle
# on crash, auto-restarts after ThrottleInterval (30s)
As long as this daemon is up, any Telegram message becomes a Claude Code CLI request on my Mac mini. The context carries over within the same chat, so it really felt like holding a live Claude Code session and talking to it.
03. Usage pattern: throw one line, and things happen on their own
From my phone I just throw a single line of free text at the Telegram bot. The bot passes it straight to the Claude CLI, and Claude spawns automation jobs on its own or queries state and sends the answer back as a reply.
# A line I often throw from my phone
Me: What's the state of today's publish queue?
Bot: (continues the same session via claude --resume)
Bot: 3 posts scheduled / 1 awaiting review / 0 incidents
Me: What's the 1 post awaiting review?
Bot: (same session, so it carries the context above)
Bot: It's a roundup of new Claude Code features. Passed the 80% first-person check.
Me: Then publish it as-is
Bot: (spawns the publish workflow → returns the result)
There are two things I really love about this.
First, the same session is kept. Even on the phone, the context doesn't break, just like the chat on my PC.
Second, without me lifting a finger, Claude spawns the automation jobs by itself. Publishing, review, stats lookups — it all wraps up inside a single line.
I've baked a few special commands into the bot. All things I use a lot.
-
/new: Start a new Claude session. Forget the previous context and start fresh -
/cancel: SIGTERM the running Claude process. For when I threw the wrong thing -
/status: Check bot status + running tasks -
/yes/no: confirm responses for destructive commands
04. I baked in a triple safety net so I can throw anything from my phone without incidents
When I first built the two-way bot, what scared me most was an incident. One wrong line thrown from my phone could wreck the entire automation system, right? So I baked in a triple gate.
a. chat_id whitelist
I didn't let the bot accept messages from just anyone. It only allows my single chat_id specified in secrets/telegram.env. Even if the bot token leaks, a message from any other chat_id is simply ignored.
b. destructive keyword detection
I bake dangerous command patterns into regex and run a match check on every message. On a match, instead of passing it straight to the Claude CLI, it asks for a /yes or /no confirm back on my phone.
# Part of DESTRUCTIVE_PATTERNS (the patterns I baked in)
DESTRUCTIVE_PATTERNS = [
r"rm\s+-rf", # wipe files entirely
r"git\s+push.*force", # force push
r"DROP\s+TABLE", # wipe a DB table
r"sudo", # system privilege escalation
# ...
]
# on match, a confirm gate
Me: clean up /tmp/* for me
Bot: ⚠️ destructive command detected: 'rm -rf'
Bot: Proceed? /yes or /no
c. destructive_gate_hook
I also baked in a hook so that even actions Claude itself judges to be destructive get blocked one more time. It's a safety net that filters cases the regex missed through Claude's own judgment. Both stages have to pass before the action actually happens.
What the triple gate means: I think if you only have a single-layer safety net, once it's breached, it's over. The key is that all three stages — chat_id (access) → regex (input pattern) → hook (execution judgment) — filter at different layers. Even if one is breached, the next catches it.
05. The first incident: the day the bot fell into an infinite loop
Less than a month after building this bot, I hit an infinite-loop incident once. The bot crashed while calling the Claude CLI, and the restarted bot processed the same message from the start. That crashed again too, and the restarted bot processed the same message again... It became a picture of the bot spamming itself.
When I looked at the cause, it was a simple critical-section bug. The point where Telegram's last_update_id gets saved was placed after the Claude CLI call. If Claude crashed mid-processing, the bot died with last_update_id unsaved, and the restarted bot received the same message again.
Right then and there I moved the last_update_id save to before the Claude CLI call. Save the moment it arrives, then process — so that even if processing fails, the same message doesn't come in again. Swapping the order of one line was the whole fix, and that one line stopped a bug that had been running the bot in spam mode for an hour! (I lost five whole days to this bug, ugh.)
06. The second incident: the day macOS took away permissions wholesale
I hit an even more absurd incident one more time. One day the bot suddenly went unresponsive, and at the same time 12 automation jobs died simultaneously with ModuleNotFoundError: No module named 'lib'.
Digging into the cause with Claude Code, it turned out Homebrew had auto-upgraded python3 from 3.12 to 3.14.5 a few nights earlier. In the process the python3 binary's signature changed, and macOS TCC re-evaluated permissions for the new binary and ended up blocking access to ~/Documents.
So when os.getcwd() was called inside a wrapper launched by launchd, it threw a silent PermissionError, which broke the import system and showed up as ModuleNotFoundError. The real cause wasn't a module problem but a permissions problem — but the surface error looked like a completely different thing.
It was only after I manually added the new python3 binary to System Settings → Privacy & Security → Full Disk Access that the bot came back to life. After that I baked in one operating rule: "On a Homebrew python upgrade, mandatory check: confirm that python3 -c 'import os; print(os.getcwd())' passes."
07. How I'm using it from my phone now
It was only after going through those two incidents that I started running this setup as part of daily life. Now the Telegram app on my phone is effectively the mobile console for my automation system. (Spot to fill in the command patterns I throw most often from my phone. e.g. reflecting ideas after ideation on the commute, checking various monitoring categories, etc.)
I've also decided what I don't do from the phone. I never edit the bot's own code from the phone. If I modify and restart the bot itself inside a Claude the bot spawned, the stdout gets cut off. Bot work always happens in the Mac desktop terminal.
08. 5 learnings from 2 months of running it
Working on this bot for nearly two months, what I got wasn't simply "how to build a Telegram bot." It was closer to learning firsthand, by hitting it directly, what a system with a daemon actually is.
1. Slipping one human gate into commands thrown from the phone is the safest thing
I don't process every command automatically. On a destructive pattern match it asks /yes /no, and that one step stops a line mis-tapped on the phone from snowballing into an incident. Bake in one assumption — "it's a phone, so a finger can slip" — and the gate designs itself naturally.
2. Checking for permission loss on a macOS system upgrade is mandatory
After the TCC incident, I never take system upgrades lightly. A single Homebrew python upgrade can kill 12 launchd jobs at once. One manual check after an upgrade is far cheaper than an hour of debugging.
3. Never edit the bot itself from inside the bot
Once, while editing the bot's code inside a Claude the bot had spawned, the bot daemon stalled. If stdout gets cut while it modifies itself, the bot can no longer receive answers. Bot edits always from an external terminal.
09. The freedom to call automation with one line from your phone
To me this bot is effectively the most expensive single line in my system. Not because the time it took to build was expensive, but because once you get used to the freedom of calling automation with one line from your phone, you can't quit it!
If you're at the stage of running several automation jobs, I'd recommend building yourself a mobile console like this. Start with a one-way notification bot, and as you get comfortable, gradually evolve it into two-way + a triple safety net. Go through an incident or two, and the safety net naturally gets sturdy.
References
This is a first-person write-up of the incident cycles I went through running this myself for nearly a year. It's not a general Telegram bot guide. I received no sponsorship of any kind from Anthropic.
Canonical: https://jessinvestment.com/how-i-built-a-telegram-bot-to-run-claude-code-from-my-phone/
Original with full infographics and visual structure: https://jessinvestment.com/how-i-built-a-telegram-bot-to-run-claude-code-from-my-phone/
Top comments (0)