SLOPSHOPPER

bridge-format

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

newprompt
A shopper browsing a rack in a slop shop
README

Claude Mobile Bridge

License: MIT Bun

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.

Features

  • Forum topics β€” each Claude Code session gets its own Telegram topic; the bot creates/deletes them as sessions come online/offline.
  • Mini App β€” browser/Telegram Mini App with Chat, Sessions, Tasks (Kanban), Status, and Agents tabs.
  • Auto-discovery β€” detects running Claude Code sessions automatically.
  • Channel relay β€” message running desktop sessions without disconnecting them.
  • Claude desktop app β€” the bridge plugin brings the relay to Code-tab sessions (which can't load channels), with a status band (expandable details) and /bridge status|doctor.
  • Live streaming β€” watch tool calls, results, permission-mode toggles, and stop-hook blocks in real time.
  • Unified chat β€” desktop TUI, Telegram topic, and Mini App share one conversation with origin labels (πŸ–₯ Desktop / 🌐 Web / πŸ’¬ Chat).
  • Voice, photos & documents β€” voice transcribed via OpenAI; photos/PDFs/text analyzed.
  • Extended thinking β€” think for deeper reasoning, ultrathink for 50k tokens.
  • Interrupt with ! β€” prefix a message to interrupt the current query.
  • Remote slash commands β€” /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 browser β€” /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 control β€” /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.
  • Verbosity control β€” /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 loops β€” /ralph <repo> runs an autonomous issue-crunching loop in a desktop terminal; distilled per-iteration beats stream to a dedicated topic.
  • Remote-answerable AskUserQuestion β€” clarifying-question cards relay to Telegram/Web; tap to answer, first answer wins.
  • Remote tool permissions β€” when a session hits a tool-approval prompt (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.
  • Cursor integration β€” bridge Cursor IDE windows into the same Telegram/Web UI.
  • MCP support β€” configure external tools in mcp-config.ts.

Quick Start

Shortcut: already have the Claude Code CLI? Clone the repo, run bun install, then open claude in 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.

1. BotFather config

  1. Open @BotFather β†’ /newbot (or select an existing bot) and grab the token
  2. Bot Settings β†’ enable "Topics in Groups"
  3. Optional: Group Privacy β†’ disable (so bot sees all messages in topics)

2. Install

Prerequisites: 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).

3. Create a forum group

The bot works best in a Telegram forum group where each session gets its own topic:

  1. Create a new Telegram group
  2. Go to group settings β†’ Topics β†’ enable topics (makes it a forum)
  3. Add your bot to the group
  4. Promote the bot to admin with Manage Topics permission (required to create/delete session topics)
  5. The bot auto-detects the forum and starts creating topics for sessions

Private chat also works β€” you get the classic UI with /list and /switch buttons. But once the bot detects a forum group, DMs are disabled to avoid split-brain.

4. Connect your desktop sessions (channel relay)

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.

  1. Register the relay as a global MCP server (replace the path with your clone location):
   claude mcp add -s user channel-relay -- bun run ~/Dev/claude-mobile-bridge/src/mcp/channel-relay/server.ts
  1. Launch each session with the relay channel β€” you need these flags every time, so alias them:
   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 bridge plugin 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.

5. tmux launcher (recommended)

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:

  • Bare 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.
  • Detach with 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.

Commands

CategoryCommands
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`

Documentation

DocWhat's in it
Channel RelayMessage running desktop sessions without disconnecting; remote-use setup
Desktop app pluginbridge (+ bridge-format): Telegram β†’ Claude desktop app (Code tab) sessions, status band, /bridge doctor
Sessions & shell scriptsAuto-discovery, /new (--branch, --resume), terminal picker, resuming offline sessions, /execute
Button menusTappable menus for /new and /claude; how to add a menu to another command
Mini AppBrowser/Telegram UI: tabs, enable, BotFather registration, HTTPS deployment, auth
Advanced hooksRemote-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 IntegrationBridge Cursor IDE windows into the same Telegram/Web UI
OperationsLogs, running as a launchd service, /settings reference
SecurityAllowlist, path validation, command safety, rate limiting, audit logging

Development

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)

Security

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.

License

MIT

Source 1 files
hooks/register.ts 49 lines
1import 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