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.