Monitor and control Claude Code from your phone. CTM is a Rust daemon that bridges Claude Code CLI sessions to Telegram, giving you a real-time mobile interface to what Claude is doing.
You (phone) Your machine
┌──────────┐ ┌──────────────────────┐
│ Telegram │◄────────────────│ CTM daemon │
│ │ │ ↕ │
│ "Running │ Telegram API │ Claude Code (tmux) │
│ tests" │────────────────►│ ↕ │
│ │ │ Your codebase │
│ [Approve]│ │ │
└──────────┘ └──────────────────────┘
What you see in Telegram:
- "Running tests" (not
🔧 Running: Bash cargo test) - "Editing config.rs" (not
🔧 Running: Edit .../config.rs) - "Searching for 'auth'" (not
🔧 Running: Grep auth) - Approve/reject tool executions with inline buttons
- Each Claude session gets its own Forum Topic thread
- Type prompts, stop, or kill Claude directly from chat
# Build
git clone https://github.com/DreamLab-AI/Claude-Code-Rust-Telegram.git
cd Claude-Code-Rust-Telegram
cargo build --release
# Configure (interactive)
./target/release/ctm setup
# Start daemon
./target/release/ctm start &
# Run Claude in tmux — everything mirrors to Telegram automatically
tmux new -s claude
claudeThis is a security-focused rewrite of the original TypeScript version. The TypeScript version had 10 security vulnerabilities including 3 CRITICAL command injection flaws. The Rust version fixes all of them:
| Vulnerability | TypeScript | Rust |
|---|---|---|
| Command injection | execSync(\tmux ... ${input}`)` |
Command::new("tmux").arg(input) |
| World-readable secrets | Default file perms | 0o600 files, 0o700 dirs |
| PID race conditions | Check-then-write | flock(2) atomic locking |
| Unvalidated callbacks | Chat ID skipped | Validated on ALL update types |
| No rate limiting | Unlimited | governor token-bucket |
| JSON panics | .unwrap() on user input |
All parsing returns Result |
- Human-readable summaries — Tool actions described in plain English, not raw operations
- Session threads — Each Claude session gets its own Telegram Forum Topic
- Auto-cleanup — Stale sessions and dead topics deleted automatically
- Send prompts — Type in Telegram, text appears in Claude's CLI
- Stop/interrupt — Send
stopto press Escape,killto send Ctrl-C - Slash commands —
cc clear,cc compactforwarded to Claude
- Inline keyboards — Approve, Reject, or Abort with one tap
- Details button — Expand to see full tool input parameters
- Timeout fallback — If you don't respond, Claude falls back to CLI prompts
Tool actions are summarized in natural language with a two-tier system:
- Rule-based (zero latency) — Covers cargo, git, npm, docker, pip, file ops, search, and 15+ system commands
- LLM fallback (optional) — Unknown tools get summarized via Haiku/Claude API
| What Claude Does | What You See |
|---|---|
Bash: cargo test |
Running tests |
Bash: cargo build --release |
Building project (release) |
Bash: git push |
Pushing to remote |
Edit: /home/user/src/config.rs |
Editing config.rs |
Grep: pattern "authentication" |
Searching for 'authentication' |
Task: {desc: "Explore auth"} |
Delegating: Explore auth |
- Message @BotFather on Telegram — it's a real bot, message it directly
- Send
/newbot, choose a name and username (must end inbot) - Save the API token
- Create a new group in Telegram
- Add your bot to the group
- Make the bot an Admin (needs: Manage Topics, Post Messages, Delete Messages)
- Enable Topics in group settings (requires Telegram desktop or mobile app, not web)
- Back in @BotFather:
/mybots-> Select bot -> Bot Settings -> Group Privacy -> Turn off
This lets the bot see all messages in the group, not just commands.
# After sending a message in the group:
curl -s "https://api.telegram.org/botYOUR_TOKEN/getUpdates" | python3 -m json.tool | grep '"id"'Supergroup chat IDs start with -100 (e.g., -1001234567890).
Run the interactive wizard:
ctm setupOr create ~/.config/claude-telegram-mirror/config.json manually:
{
"bot_token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"chat_id": -1001234567890,
"enabled": true,
"use_threads": true,
"verbose": true
}Both snake_case and camelCase field names are accepted.
Add to ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [{ "hooks": [{ "type": "command", "command": "ctm hook", "timeout": 5000 }] }],
"PostToolUse": [{ "hooks": [{ "type": "command", "command": "ctm hook", "timeout": 5000 }] }],
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "ctm hook", "timeout": 5000 }] }],
"SessionStart": [{ "hooks": [{ "type": "command", "command": "ctm hook", "timeout": 5000 }] }],
"SessionEnd": [{ "hooks": [{ "type": "command", "command": "ctm hook", "timeout": 5000 }] }],
"Notification": [{ "hooks": [{ "type": "command", "command": "ctm hook", "timeout": 5000 }] }],
"Stop": [{ "hooks": [{ "type": "command", "command": "ctm hook", "timeout": 5000 }] }]
}
}ctm start & # Start daemon in background
ctm status # Verify it's running
ctm doctor # Run diagnosticsctm start # Start bridge daemon (foreground)
ctm hook # Process hook events from stdin (called by hooks)
ctm status # Show daemon status and active sessions
ctm doctor # Run diagnostics
ctm doctor --fix # Auto-fix permission issues
ctm setup # Interactive setup wizard| Input | Action |
|---|---|
| Any text | Sends as input to Claude |
stop / esc |
Sends Escape (pause Claude) |
kill / ctrl-c |
Sends Ctrl-C (exit Claude) |
cc clear |
Sends /clear to Claude |
cc compact |
Sends /compact to Claude |
/status |
Show active sessions |
/help |
Show commands |
/ping |
Health check |
# Required
export TELEGRAM_BOT_TOKEN="your-token"
export TELEGRAM_CHAT_ID="-1001234567890"
export TELEGRAM_MIRROR=true
# Optional
export TELEGRAM_MIRROR_VERBOSE=true # Show tool start/result (default: true)
export TELEGRAM_USE_THREADS=true # Forum topics per session (default: true)
export TELEGRAM_AUTO_DELETE_TOPICS=true # Delete topics on session end
export TELEGRAM_TOPIC_DELETE_DELAY_MINUTES=1440 # Delay before topic deletion (default: 24h)
export TELEGRAM_SESSION_TIMEOUT=1800 # Seconds before session goes stale
export TELEGRAM_RATE_LIMIT=20 # Messages per second
# LLM-powered summaries (optional)
export CTM_LLM_SUMMARIZE_URL=https://api.anthropic.com/v1/messages
export CTM_LLM_API_KEY=sk-ant-...Environment variables override config file values.
| Field | Type | Default | Description |
|---|---|---|---|
bot_token |
string | required | Telegram bot API token |
chat_id |
integer | required | Telegram supergroup chat ID |
enabled |
bool | false |
Enable mirroring |
verbose |
bool | true |
Show tool start/result messages |
use_threads |
bool | true |
Create Forum Topics per session |
auto_delete_topics |
bool | true |
Delete topics when sessions end |
topic_delete_delay_minutes |
integer | 1440 |
Minutes to wait before deleting |
session_timeout |
integer | 30 |
Seconds of inactivity before stale |
rate_limit |
integer | 1 |
Max messages per second |
llm_summarize_url |
string | none | LLM endpoint for summary fallback |
llm_api_key |
string | none | API key for LLM endpoint |
Run CTM on multiple machines with a shared Telegram group:
- Create one bot per machine via @BotFather
- Add all bots to the same supergroup with admin permissions
- Each daemon uses its own bot token and only manages its own topics
┌─────────────┐ hooks ┌─────────────┐ NDJSON ┌─────────────┐
│ Claude Code │───(stdin/out)──│ ctm hook │───(socket)───│ Bridge │
│ (tmux) │ └─────────────┘ │ Daemon │
│ │◄──(send-keys)──────────────────────────────│ │
└─────────────┘ │ ┌───────┐ │
│ │ Bot │ │
┌─────────────┐ │ │(telo- │ │
│ Sessions │◄────────(SQLite)──────────────────────────│ │ xide) │ │
│ (.db) │ │ └───┬───┘ │
└─────────────┘ └─────┼─────┘
│
Telegram API
│
┌─────┴─────┐
│ Telegram │
│ Group │
│ (Topics) │
└───────────┘
| Module | Lines | Responsibility |
|---|---|---|
bridge.rs |
~1100 | Central orchestrator: routes messages between all components |
bot.rs |
~300 | Telegram API: send/receive, forums, inline keyboards, rate limiting |
socket.rs |
~250 | Unix socket server with flock PID locking, NDJSON protocol |
session.rs |
~250 | SQLite persistence: sessions, approvals, stale cleanup |
formatting.rs |
~700 | Tool summaries, message formatting, ANSI stripping, chunking |
summarizer.rs |
~150 | Optional LLM fallback for unknown tool summarization |
hook.rs |
~250 | Converts Claude Code hook events to bridge messages |
injector.rs |
~200 | Shell-safe tmux command injection via Command::arg() |
config.rs |
~300 | Config loading: env vars > file > defaults, permission enforcement |
types.rs |
~200 | Shared types: BridgeMessage, HookEvent, Session, enums |
error.rs |
~50 | Error types via thiserror |
ctm doctor # Run all diagnostics
ctm doctor --fix # Auto-fix permissions and config| Problem | Solution |
|---|---|
| Bridge won't start | Another instance holds the flock. Kill it or check ctm status |
| No messages in Telegram | Run ctm doctor. Check bot token, chat ID, admin perms, Topics enabled |
| tmux injection broken | Ensure Claude Code runs inside tmux: tmux list-sessions |
| 409 Conflict | Two daemons polling same bot token. Each machine needs its own bot |
| getUpdates timeout | Network issue. Outbound messages still work; inbound polling retries automatically |
| Bot can't see messages | Disable Group Privacy via @BotFather |
cargo build # Debug build
cargo build --release # Optimized release build (~8MB binary)
cargo test # 21 tests
cargo clippy # 0 warnings
cargo fmt --check # Check formatting
RUST_LOG=debug ctm start # Verbose logging| Document | Description |
|---|---|
| Architecture | System diagrams, message flows, concurrency model |
| Setup Guide | Step-by-step installation and configuration |
| Development | Building, testing, module deep dives |
| Security | Threat model, vulnerability fixes, audit checklist |
| Product Requirements | Full PRD with security vulnerability matrix |
| ADR | Decision |
|---|---|
| ADR-001 | Rust over TypeScript |
| ADR-002 | teloxide for Telegram API |
| ADR-003 | Bash fire-and-forget hooks |
| ADR-004 | flock(2) atomic PID locking |
| ADR-005 | governor token-bucket rate limiting |
| ADR-006 | Single binary with clap CLI |
MIT