← Back to projects

I run Claude Code on a headless server and wanted to send it tasks from my phone. The first version was Python, and it worked, but it leaked memory, started slowly, and had a session-handling bug I could not reproduce. Rewriting it in Rust fixed all three problems and cut resource usage by an order of magnitude.

A single binary that stays up indefinitely as a systemd service. Session state persists across restarts. Markdown formats correctly in Telegram. Voice messages transcribe locally via whisper.cpp. The only dependency at runtime is Claude Code itself.

Async Message Flow teloxide · tokio · comrak · sd-notify inbound Telegram API polling Dispatcher teloxide Handlers /new · text · voice Claude CLI subprocess Session Store JSON file auto-retry on expiry outbound JSON Parse session_id + text Markdown → HTML comrak AST Message Split 4096 char limit Telegram API response plain-text fallback if HTML fails
01

Session persistence across restarts

Sessions live in a JSON file mapping Telegram user IDs to Claude session IDs. The format is compatible with the Python predecessor for zero-migration switchover. On startup it loads into an Arc<Mutex<>>. If Claude returns a session-expired error, the handler removes the stale entry and retries with a fresh session automatically.

02

Markdown to Telegram HTML

Comrak parses the response into a CommonMark AST. A custom walker converts it to the subset of HTML that Telegram supports: bold, italic, code blocks, pre, links. Messages over 4096 characters split at paragraph boundaries with HTML tags closed and reopened across chunks. If the HTML still fails Telegram's parser, the handler strips formatting and sends plain text.

03

systemd-notify watchdog

The binary registers as Type=notify with systemd. A background tokio task pings the Telegram API every 60 seconds and calls sd_notify on success. After 10 consecutive polling failures, the process exits. systemd restarts it automatically. No external monitoring scripts; the health check is part of the binary.

04

78x faster by rewriting in Rust

The Python predecessor took 233ms to start and used 55 MB of memory. The Rust version starts in 3ms and sits at 7 MB. Markdown conversion runs 26x faster. For a service running around the clock on a home server, the resource savings add up. The port also caught a session-handling race condition that Python had been silently ignoring.

// minimal deployment

Minimal deployment

One binary, one .env file, one systemd unit. No runtime, no container, no package manager dependency. cargo install builds it, systemctl manages it.

// self-healing error paths

Self-healing error paths

Expired sessions are detected and retried. HTML formatting failures fall back to plain text. Polling failures accumulate until the watchdog exits and systemd restarts. From the user's side, problems show up as a brief delay, not an error message.

// multi-user path

Multi-user path

Session isolation is already keyed by Telegram user ID. Adding rate limiting per user, a queue for concurrent Claude invocations, and proper multi-user access control would turn this from a personal tool into something deployable for a small team.

Rusttokioteloxidecomraksystemdserde
// the code

Read the source, run it locally, open issues.