Companion to bridge: shows relayed Telegram prompts as π± <message>; the channel tag goes to the model as hidden context.

Control Claude Code sessions from your phone via Telegram. Add the bot to a forum group and each session gets its own topic thread β isolated conversations, live streaming, no context switching.
bridge plugin brings the relay to Code-tab sessions (which can't load channels), with a status band (expandable details) and /bridge status|doctor.think for deeper reasoning, ultrathink for 50k tokens.! β prefix a message to interrupt the current query./clear, /compact, /context, and /model typed straight into the desktop session's TUI (/model picks a model, injecting /config model=β¦ to switch the live session). Run the session under tmux and injection uses tmux send-keys β no accessibility, works in any terminal (Cursor included)./skills surfaces the session's Claude Code skills and slash commands (user/project/plugin) with recents, search, and origin-group drill-down; tap to inject the chosen one β with args β into the desktop TUI./tmux opens a button panel of your sessions (peek Β· kill Β· start); /peek shows a session's live terminal screen as a snapshot with π refresh. Run several sessions in one folder with the cct launcher β each gets its own topic, routed by a stable per-session id./verbose 0|1|2 (or the π Verbosity row in /settings) dials how much streams to a topic: quiet (final text only), normal, or detailed./ralph <repo> runs an autonomous issue-crunching loop in a desktop terminal; distilled per-iteration beats stream to a dedicated topic.Bash wants to run β¦), the desktop dialog is untouched and a π card also appears in that session's topic; answer from either, first answer wins. Pairs with --dangerously-skip-permissions (the default for /new): a PreToolUse hook returning ask still prompts through bypass, so if you gate rm -rf and friends that way, those β and only those β reach your phone.mcp-config.ts.Shortcut: already have the Claude Code CLI? Clone the repo, run
bun install, then openclaudein the repo and run/setupβ it walks the whole flow below interactively (.env, relay registration, optional hooks), idempotently. The manual steps follow for reference or if you'd rather do it by hand.
/newbot (or select an existing bot) and grab the tokenPrerequisites: Bun 1.0.23+, Claude Code CLI, Telegram Bot Token
Optional: tmux (brew install tmux) β /new sessions then run under it, which lets the bot inject /clear, /compact and /context via tmux send-keys instead of terminal-specific keystrokes. Everything works without it. See docs/channel-relay.md.
git clone https://github.com/azaidi94/claude-mobile-bridge.git
cd claude-mobile-bridge
bun install
cp .env.example .env # Edit with your credentials
cp mcp-config.example.ts mcp-config.ts # Optional: configure MCP tools
bun run start
Required .env variables:
TELEGRAM_BOT_TOKEN=your-bot-token
TELEGRAM_ALLOWED_USERS=123456789 # Your Telegram user ID (get from @userinfobot)
See .env.example for all options (working dir, allowed paths, voice transcription, rate limits, etc).
The bot works best in a Telegram forum group where each session gets its own topic:
Private chat also works β you get the classic UI with
/listand/switchbuttons. But once the bot detects a forum group, DMs are disabled to avoid split-brain.
The bot can run its own sessions out of the box, but the main use case β messaging the Claude Code session already running on your desktop, without disconnecting it β needs the channel relay. Discovery can see a plain claude process, but only a relay-enabled session can be driven from your phone.
claude mcp add -s user channel-relay -- bun run ~/Dev/claude-mobile-bridge/src/mcp/channel-relay/server.ts
claude --dangerously-skip-permissions --dangerously-load-development-channels server:channel-relay
# or add to ~/.zshrc / ~/.bash_profile:
alias cc='claude --dangerously-skip-permissions --dangerously-load-development-channels server:channel-relay'
Claude Code shows a one-time menu (βI am using this for local developmentβ vs βExitβ). Choose 1, press Enter.
Claude desktop app (Code tab): you can't pass launch flags there. Install the
bridgeplugin instead β the relay hands it messages over a local socket.
That session now appears in /list with a π‘ indicator and is messageable from its topic. See Channel Relay for remote/headless launching (you're not at the Mac) and how it works.
That's the full setup. For optional integrations (remote-answerable questions, exact /clear follow, Cursor, the Mini App), see Documentation below.
Running sessions inside tmux unlocks the best experience: reliable /clearΒ·/compactΒ·/context injection (the bot types them via tmux send-keys β no accessibility, works in any host terminal incl. Cursor), plus the /tmux panel and /peek screen capture. The shipped launcher, scripts/tmux/launch.sh, wraps claude in a tmux session on a dedicated claude socket and lets you run multiple sessions per folder, each routed to its own topic.
Add a cct function to your shell profile (~/.bash_profile / ~/.zshrc), pointing at your clone:
cct() {
source "$HOME/Dev/claude-mobile-bridge/scripts/tmux/launch.sh"
cc_tmux_launch "$#" \
--dangerously-skip-permissions \
--dangerously-load-development-channels server:channel-relay "$@"
}
Then cct in any project starts a relay-enabled session under tmux. Behaviour:
cct β reattaches to a detached session in that folder (your work, left running when you closed the terminal), or creates a new one if none / if the only sessions there are already attached (so a 2nd cct gives you a parallel sibling, not a mirror).CCT_MODE (env) overrides: hybrid (default) Β· attach (always one per folder) Β· create (always fresh). CLAUDE_CODE_TMUX_FRESH=1 cct forces create for one launch.Ctrl-b d; the session stays alive and messageable.Multi-session routing relies on the SessionStart identity hook (see Advanced hooks) β install it so N sessions in one folder each resolve to their own topic.
If you don't use tmux, injection still works via fallbacks: a Cursor accessibility path (bind CURSOR_FOCUS_CHORD β default ctrl+alt+cmd+t β to workbench.action.terminal.focus in Cursor; see .env.example) and the cmux path. Both are more fragile than tmux and fail closed rather than risk typing into the wrong window.
| Category | Commands | ||
|---|---|---|---|
| Sessions | /list, /new (menu: folder / branch / resume), /sessions, /kill, /respawn, /rename | ||
| Control | /stop, /retry, /status, /restart | ||
| Inject | /claude (menu), /clear, /compact, /context, /skills, /model | ||
| tmux | /tmux (panel), /peek (screen) | ||
| Automation | /ralph <path> [N], `/ralph at\ | every, /cron` | |
| Files | /pwd, /cd, /ls | ||
| Quota | /usage | ||
| Scripts | /execute | ||
| Mini App | /app | ||
| Settings | /settings, `/verbose 0\ | 1\ | 2` |
| Doc | What's in it |
|---|---|
| Channel Relay | Message running desktop sessions without disconnecting; remote-use setup |
| Desktop app plugin | bridge (+ bridge-format): Telegram β Claude desktop app (Code tab) sessions, status band, /bridge doctor |
| Sessions & shell scripts | Auto-discovery, /new (--branch, --resume), terminal picker, resuming offline sessions, /execute |
| Button menus | Tappable menus for /new and /claude; how to add a menu to another command |
| Mini App | Browser/Telegram UI: tabs, enable, BotFather registration, HTTPS deployment, auth |
| Advanced hooks | Remote-answerable AskUserQuestion bridge + exact /clear follow (SessionStart) |
| Ralph loops | /ralph β run afk_tasks.sh from Telegram: start/watch/stop, verbose mode, customization |
| Skills browser | /skills β browse/search skills & slash commands, inject into the desktop TUI with args |
| Cursor Integration | Bridge Cursor IDE windows into the same Telegram/Web UI |
| Operations | Logs, running as a launchd service, /settings reference |
| Security | Allowlist, path validation, command safety, rate limiting, audit logging |
bun run dev # Run with file watching
bun run typecheck # TypeScript type checking
bun run test # Run all tests (isolated per-file to avoid state leaks)
See SECURITY.md: user allowlist, path validation, command safety checks, rate limiting, and audit logging. Mini App request auth (Telegram initData HMAC) and the LAN-bypass flags are covered in Mini App auth.
MIT
hooks/register.ts 49 lines1import type { Register } from "claude-code";
2
3/**
4 * Companion to bridge. The desktop app gives the channel card only to a real
5 * channel origin, which no hook may set, so a relayed prompt would show its
6 * raw `<channel>` tag. This shows the body alone and hands the model the tag
7 * as context.
8 *
9 * It has to be its own plugin: `$.prompt.submit` takes no `context`, and the
10 * engine's re-entry rule keeps a plugin's own hooks off the prompts it
11 * submits β so bridge can neither attach the context nor rewrite its prompt.
12 */
13
14/** Plugins whose prompts are relayed Telegram messages (channel-pull: the old name). */
15const SUBMITTERS = new Set(["bridge", "channel-pull"]);
16
17const CHANNEL_PROMPT =
18 /^(<channel source="channel-relay"(?: [A-Za-z0-9_]+="[^"]*")*)>\n([\s\S]*)\n<\/channel>$/;
19
20/** What the transcript shows, and what only the model reads. */
21export const splitChannelPrompt = (
22 text: string,
23): { shown: string; context: string } | undefined => {
24 const m = CHANNEL_PROMPT.exec(text);
25 if (!m) return undefined;
26 return {
27 shown: `π± ${m[2] ?? ""}`,
28 context:
29 `The prompt above was relayed from Telegram by channel-relay: ${m[1]} />. ` +
30 'Treat it as a <channel source="channel-relay"> message: the user is on ' +
31 "their phone, so answer as terminal text and ask multiple-choice questions " +
32 "with the ask_remote tool, never AskUserQuestion.",
33 };
34};
35
36export const register: Register = (on) => {
37 on("prompt.submit", ($, e, next) => {
38 const isRelayed =
39 e.origin.kind === "plugin" && SUBMITTERS.has(e.origin.name);
40 const split = isRelayed ? splitChannelPrompt(e.text) : undefined;
41 if (!split) return next(e);
42 return next({
43 ...e,
44 text: split.shown,
45 context: [...(e.context ?? []), split.context],
46 });
47 });
48};
49