I Built a Telegram Remote for Claude Code, Then Let It Build Itself
For a while now I've been coding from my phone during commutes, SSH'd into my VM through Termius, running Claude Code directly in a mobile terminal. It works, technically. It is also miserable. A full-screen TUI redrawing itself on a six-inch screen, permission prompts you have to squint at, losing your scroll position every time the pane repaints. Termius earned its keep, but it was never going to be a real way to work.
Then I had dawats three days straight, Wednesday through Friday. If you haven't heard the word, a dawat is basically a hosted invitation, a family or community gathering you actually have to show up and be present for, not something you sneak a laptop open during. Three consecutive days of that meant close to zero real computer time, and my project pipeline (the World Cup bracket app, cv-roast, a few others) does not pause for anyone's social calendar. Phone-only coding stopped being an occasional annoyance and became the only option I had. That's the itch that actually got scratched. Termius was the wrong tool for that job, so I built the right one: Tgmux, a Telegram bridge to Claude Code sessions running in tmux on my VM.
The core idea: don't fight the TUI, read it
The first real design decision was refusing to treat this as a headless
automation problem. A one-shot claude -p call takes a prompt and gives you
an answer, no follow-up turn, which is useless for an actual build
conversation where the agent asks questions and you reply hours later from a
different city. What I needed was a persistent interactive claude REPL,
kept alive in a tmux session per project, that I could talk to asynchronously
without ever needing to reattach a terminal by hand.
Telegram (phone) <-> Tgmux daemon (Python, VM) <-> tmux: proj-<slug>
claude REPL
dev server :30xx
Each project gets its own proj-<slug> tmux session with an allocated port
in a pool, so five agents can be mid-build at once without colliding. The
daemon never makes me SSH in directly. My Telegram messages become
tmux send-keys, and whatever the agent renders back becomes a Telegram
message.
That last part is the genuinely interesting bit. Claude Code is a full-screen
TUI, so raw pipe-pane streaming is redraw soup, cursor jumps, partial
repaints, the same line rewritten five times a second. Instead, the daemon
polls capture-pane every two seconds, strips it down with a battery of
regexes (ANSI codes, box-drawing borders, spinner glyphs, "esc to interrupt",
token counters, the live input-box echo of whatever I'm mid-typing), diffs
the cleaned result against the last snapshot it relayed, and sends only
what's new, coalesced so I don't get spammed one Telegram message per
keystroke. The snapshot baseline is saved to disk per agent, so restarting
the daemon never re-sends old output or silently drops new output, it just
picks the diff back up where it left off.
Two more pieces make it feel like a real remote control instead of a chat log:
- Question detection. The daemon watches the stable tail of the pane for
actual blocking prompts, permission dialogs,
(y/n), numbered menus, as opposed to normal conversational text, and only pings me with quick-answer buttons when the agent is genuinely stuck waiting. Ordinary progress updates just arrive quietly. - Mode control by reading the status line. Claude Code shows its current
mode (normal, auto-accept, plan) in the status line at the bottom of the
pane. There's no API for "set mode directly," so
/mode autoreads that status line, sends Shift+Tab, re-reads the status line, and repeats up to four times until it actually reports back "auto." It's controlling a TUI blind, entirely by reading its own rendered output back and closing the loop against that.
Shipping is gated on purpose: /push commits and pushes a dev branch
(creating the GitHub repo the first time) and hands back a Vercel preview
URL, but going to production needs an explicit /deploy and a Telegram
confirm button. The CLAUDE.md injected into every project also tells the
agent directly never to deploy on its own. Every command, push, and deploy
gets appended to a JSONL audit log, and the bot answers exactly one Telegram
user ID, silently ignoring everyone else.
Then I used it to build the rest of itself
Once the base loop worked, spawn a session, relay text both ways, I stopped opening a laptop for this project at all. I adopted Tgmux's own tmux session as just another agent inside itself and kept building it entirely from the phone, the same way I'd build any other project through it. Every feature after the first version was requested and shipped through the exact same Telegram thread it was improving:
| What I typed | What shipped |
|---|---|
| "add a uptime command that shows daemon uptime" | /uptime |
| "make a command to kill agents" | /kill, /killall |
| "add some emojis before the commands so they're easily readable" | emoji-prefixed command list |
| "name this project Tgmux, push to GitHub, add a README" | repo created via gh, README written |
| "can i resume a session if i kill it? ok add revive" | /revive, respawns a killed agent with --continue |
| "add a /merge command, not all projects have vercel" | /merge, a git-only dev to main release |
| "I saw a button, will you improve the ux" | quick-answer buttons on prompts, tap actions on /list |
| "/mode auto should trigger auto directly, not blind cycling" | /mode takes a target and reads the status line to confirm it |
One of those had a real near miss behind it. Partway through, I attached a
couple of test photos to see how file uploads worked, and one of them
nearly ended up swept into a git commit before I caught it and reverted.
That's what actually prompted the fix right after: every project's file
upload now auto-adds its incoming/ folder to .gitignore, because that
folder is for feeding files to the agent, not for publishing them.
Everything in that table shipped in one sitting, on the same day, entirely dictated from a phone through the exact bot being modified. The tool got good enough to build the rest of itself before I ever needed to sit at a desk for it.
Where it's at now
It stopped being a travel workaround and became the default. Right now, in
one VM, Tgmux has live or recent sessions for this portfolio, the World Cup
bracket app, cv-roast, and a couple of other side projects, switching
between them is just /switch <name> or replying to whichever agent's
message came in. If you want the honest proof of how far that went: this
post itself, the request to write it, the back-and-forth about what to
include, all of it came in over the same Telegram thread, to an agent
running inside a Tgmux session named portfolio. The dawats are long over.
I still haven't gone back to coding any other way.
Keep reading
Jul 6, 2026
I Built a Site That Roasts Your CV
A weekend idea: what if a CV review felt less like feedback and more like getting dragged by four different people who all hate your resume for different reasons. 233 roasts and 23 battles later, here's the build behind getroasted.live: the model fallback chain, the persona system, and the guardrails that keep 'savage' from turning into 'reported'.
Jun 26, 2026
2,147,483,648 World Cups, One Weekend
The 2026 World Cup was coming up and I wanted to build something themed around it, but not another 'predict the winner' app. I kept coming back to probability: the bracket is fixed, so every outcome can be computed exactly. Here's how Quantum Bracket got built, the math behind it, and the weekend it took to ship.