← Home

I replaced SSH + tmux with a Telegram bot

I run long-lived AI coding agent sessions on a server. Claude Code working through a refactor. Codex running a migration. Gemini exploring a design space. These sessions take minutes to hours. I don't want to sit in front of a terminal the entire time.

The old workflow was SSH into the server, attach to tmux, scroll through output, figure out if the agent was waiting for input or still running. If it asked a question 20 minutes ago, those 20 minutes were wasted. If I was on the train, I couldn't do anything about it.

The bridge

OpenDray's Telegram bridge isn't a notification system. It's a bidirectional terminal interface. When an agent session goes idle — meaning the CLI has stopped producing output for a configurable threshold (default 8 seconds) — the bridge sends the latest content to your linked Telegram chat.

For Claude Code specifically, it does something more interesting. Claude writes structured JSONL output to a log file in the session's working directory. The bridge reads this file directly, extracts the last assistant response, and detects what kind of prompt Claude is showing: free text, yes/no, numbered options, or multi-select checkboxes.

Each prompt type gets a different Telegram representation. A yes/no question becomes two inline buttons. A numbered list becomes a column of buttons. A multi-select (like Claude's permission approval dialog) becomes checkboxes you can toggle, with a submit button at the bottom. Your selection is formatted exactly as the CLI expects it on stdin.

How linking works

You send /link 3 to the bot. That binds your Telegram chat to session 3. From that point, any plain text you send in the chat goes to the agent's stdin. Agent output streams back in 2-second batches. Reply to any idle notification and it routes to that session automatically — you don't even need to be in a linked chat.

Quick keys work too: /cc sends Ctrl+C, /cd sends Ctrl+D, /tab cycles completions, /yes and /no do what you'd expect. The /screen command captures the current terminal state as an HTML-rendered snapshot — useful when the agent is mid-output and you want to see the full picture, not just the tail.

Event-driven, not polling

The implementation hooks into the session hub's idle detection. When the PTY ring buffer goes quiet, the hub fires an onIdle event through a hook bus. The Telegram notifier subscribes to this event and calls the forwarder, which diffs the current output against the last snapshot it sent. If there's genuinely new content (above a 5-rune threshold), it sends the message. If nothing meaningful changed, it stays quiet.

No polling loops. No timers. The only periodic operation is Telegram's own long-poll for inbound messages, which blocks efficiently server-side.

The result

I start a Claude session from the OpenDray app on my phone. I give it a task. I put the phone in my pocket. Ten minutes later, a Telegram message arrives: Claude finished the refactor and is asking whether to commit. I tap "yes." It commits. I never opened a terminal.

The Telegram bridge was supposed to be a weekend feature. It became the way I use OpenDray 60% of the time.