Self-hosted Telegram bot that turns your phone into a remote control for a local Claude Code CLI session. One bot per project, owner-controlled pairing, multi-user broadcast, structured event streaming, inline permission prompts, a read-only multi-tab web viewer, and a localhost-app preview proxy through a user-supplied tunnel.
Pre-release. The architecture and phased build plan live in claude-code-remote-plan.md. The active ticket queue is in BACKLOG.md; finished and closed tickets archive to DONE.md.
The chat-bot loop is functional today: pairing, session lifecycle (/new, /stop, /continue), structured event broadcast, MCP-driven inline permission prompts, AskUserQuestion replies, slash-command passthrough, and /cost / /usage summaries. The web viewer, the localhost preview proxy, and the /view / /last / /preview URL-minting commands are scoped in CCR-012 – CCR-015 and not yet implemented.
Only paired users can run any of these except /start. Plain text (no leading /) is forwarded to the active session as a user turn, or starts a fresh session if none is running.
| Command | Effect |
|---|---|
/new |
Start a fresh Claude session (no initial prompt). If a session is running, stop it, broadcast a "— — — new session — — —" divider to all paired chats, then start fresh; idle clears stay quiet (no divider). |
/stop |
Stop the active session. Idempotent on idle. |
/continue [<id8>] |
Resume the most recent finished session, or the one whose claude_session_id[:8] matches the prefix shown in /sessions. |
/pid |
Show the active session's id, OS pid, and uptime. |
/sessions |
List the most recent 20 sessions (newest first). Sessions still initialising ([idle] — no claude_session_id yet) are filtered out. |
/rename <id8> <name> |
Rename the session whose claude_session_id[:8] matches the prefix. When the prefix matches a resume chain, every row in the chain is renamed in one commit. |
/rename current <name> |
Rename the active session's chain without typing the prefix. Replies with a stable error if no session is active or the active session is still in [idle]. |
/answer <id8> <text> |
Reply to a free-text AskUserQuestion by 8-hex prefix (the question's tool_use_id, not a session id). Inline-button questions are answered by tapping. |
| Command | Effect |
|---|---|
/start |
First sender bootstraps as owner; later senders request access (owner gets a DM with the approval command). |
/who |
Pairing status — owners see the full table; friends see the count plus the owner handle. |
| Command | Effect |
|---|---|
/cost |
Per-session usage summary computed locally from the JSONL log (turns, tokens, tool calls, elapsed, cost). |
/usage |
Most recent rate-limit snapshot (window, status, reset time, overage state). |
/agents |
Subagents the active session has spawned, plus the agent library under .claude/agents/. |
/skills |
Skills reported by the active session's system/init event. |
/model and /compact are forwarded to Claude as a user turn — Claude Code interprets the leading slash exactly as if you typed it in its TTY.
/mcp and /init are blocked: those are interactive-only commands and the bot will tell you to run them in your local terminal.
Anything else not in this list returns the canonical usage hint. /view, /last, and /preview appear in that hint but are not yet implemented (CCR-014).
- Python 3.12+ and
uv. - A Telegram bot token from
@BotFather(/newbot). - (One-time)
uv syncanduv run python -m ccr init-db.
cp .env.example .env # if you don't have one yet
# edit .env: paste your bot token into TELEGRAM_BOT_TOKEN
# JWT_SECRET can be generated with: openssl rand -hex 32PUBLIC_URL is unused until CCR-012 (the web server); keep the placeholder.
uv run python -m ccr serveYou should see Bot started, awaiting updates in the logs. Leave it running.
In Telegram, open your bot and send /start. With no owner paired yet, the bot replies:
Bootstrap pairing.
Telegram ID: <your-id>
Code: <8-char-code>
On the project host:
python -m ccr pair approve <code>
Run that command in a second terminal:
uv run python -m ccr pair approve <code>It prints Approved Telegram user <id> (owner). Send /start again — the bot now replies:
Paired. Send a prompt to start, or /new for a fresh session.
From a different (unpaired) Telegram account, send any plain text to the bot. Expected reply:
Not paired. Send /start to request access.
The owner gets a DM only after the unpaired user sends /start:
Pairing request from @<username> (id <id>).
Approve with: python -m ccr pair approve <code>
uv run python -m ccr console opens a REPL with pair list, pair pending, pair approve <code>, pair revoke <id>, pair invite <id> [--label X], status, help. The same operations are available as one-shot subcommands:
uv run python -m ccr pair list
uv run python -m ccr pair pending
uv run python -m ccr pair approve <code>
uv run python -m ccr pair revoke <tg-user-id>
uv run python -m ccr pair invite <tg-user-id> --label friendOwner cannot be revoked. The first approved pairing auto-promotes to owner.
src/ccr/ Python package (CLI, bot, web, claude wrapper, console, db).
tests/ Pytest suite mirroring src/ccr.
data/ Runtime SQLite + JSONL session logs (gitignored).
templates/ `dev-stack-agents` git submodule.
uv sync # install runtime + dev deps
uv run pytest # full suite
uv run ruff check src tests
uv run ruff format --check src tests
uv run mypy src
uv run pre-commit run --all-files