Claude Code on My Mac, Driven From My Phone: Remote Control, a Telegram Bot and SSH

Claude on the phone starts in a clean cloud sandbox. Here is how I reach the full local Claude Code setup on my Mac instead, the four failures that took it down, and a prompt that builds it for you.

My Claude Code setup lives on one Mac: skills, MCP servers, memory files, Keychain secrets, a logged-in Chrome, local builds, and the Obsidian vault my life-admin assistant works from. Claude on the phone gives you a cloud container with a fresh clone of one repository and none of that. Away from the desk, I want the Claude on that Mac. I reach it three ways.

What using it looks like

A phone on a cafe table with a chat bubble, linked by a glowing thread through an arched window to the home desk where the laptop and the orange spark do the work

On a walk I open the Claude app, pick the environment mac-Active and type. The session runs on the Mac at home, in my projects folder:

Claude app · mac-Active

the blog cards overflow on mobile. fix it, check at 390px and deploy

edit src/components/PostList.astroChrome: open localhost:4321 at 390pxnpm run deploygit push

Fixed: the grid columns couldn't shrink below their content. Checked each page at 390px, nothing wider than the screen. Deployed and pushed.

Typed on the phone, run on the Mac with its skills, git credentials and Chrome.

For a folder the permanent bridge doesn’t cover, I tap 🖥 Claude in Telegram, pick the folder and get a link to a fresh environment there. For a long build, a log to tail or a password prompt, I SSH in with Termius and attach to a tmux session that launchd keeps open.

The architecture

Three paths from a phone to Claude Code on a Mac: Claude app via Remote Control, Telegram bot, SSH over TailscalePHONETRANSPORTMAC (always on)Claude appsessions on "mac-Active"Telegram🖥 Claude → pick a folderTermiusSSH with a keyclaude.ai relayTelegram Bot APITailscale (private)launchd: remote-control--name mac-Active --chromebot → remote-controltg-<folder> per projectsshd + tmux "mac"attach and run anythingsame skills, MCP servers,memory, Chrome, vaultcaffeinate · watchdog
Three ways in, one machine doing the work. Nothing runs in a cloud sandbox: every path ends in my local environment.
  1. Claude app → Remote Control. launchd runs claude remote-control on the Mac, which registers an environment. The app lists it next to the cloud environments, and its sessions run locally.
  2. Telegram bot → Remote Control in any folder. A private bot starts one more bridge per folder on demand and sends back the link.
  3. Termius → SSH over Tailscale. A private network between phone and Mac, key-only SSH, and a tmux session started at login.

Layer 1: a bridge that never sleeps

A laptop glowing on a desk at night like a tiny lighthouse, a steady orange bridge arcing from it to the moon while the cat sleeps on the chair

Remote Control is a long-running process, and when it dies the environment drops out of the app. I let launchd own it:

~/Library/LaunchAgents/com.example.claude-rc.plist (core)
<key>ProgramArguments</key>
<array>
<string>/Users/you/.local/bin/claude</string>
<string>remote-control</string>
<string>--name</string><string>mac-Active</string>
<string>--chrome</string>
</array>
<key>WorkingDirectory</key><string>/Users/you/Projects/Active</string>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
  • KeepAlive restarts the bridge whenever it exits.
  • --chrome lets sessions drive my real Chrome through the Claude in Chrome extension, so from the phone I can check a page, fill a form or read a dashboard I’m logged into.
  • --name mac-Active sets the label in the app’s environment list.

launchd doesn’t expand ~, so the plist needs absolute paths.

Layer 2: a Telegram bot that opens Claude in any folder

A paper-plane bird carrying a key to a row of folders; one folder is open and the orange spark glows out of it

One permanent bridge covers one folder, and my projects sit in many. Starting another bridge takes a shell on the Mac, so I added a button to a private Telegram bot that already runs there.

🤖my-mac-bot

🖥 Claude

Where should I open Claude? 🟢 already running (I'll resend the link), ⏹ stop it.

🟢 ShopApp⏹▶️ Blog▶️ Documents📂 Pick another folder…
🤖my-mac-bot

📂 ~/Projects/Side

⬆️ Back📁 GameJam📁 Scripts▶️ Start Claude here

🖥 Claude in GameJam is ready. Open it and tap “new session”:

The whole flow is three taps: 🖥 Claude, a folder, the link. The bot answers only its owner. (Folder names are made up.)

The bot is plain Python with no dependencies. The Remote Control module is about 140 lines, self-check included. Starting a bridge comes down to this:

remote.py (core)
def start(path: Path, timeout: int = 45) -> str:
"""Returns the environment link; if already running, the existing link."""
if reason := why_not(path): # not a folder, home dir, or the permanent one
raise RuntimeError(reason)
key = slug(path) # "GameJam-1a2b3c": folder name + hash of the path
if pid(key) and (u := url(key)):
return u
log = LOGS / f"{key}.log"
cmd = (f"'{CLAUDE}' remote-control --name tg-{key} --chrome 2>&1"
f" | {{ dd bs=1 count=65536 of='{log}'; cat > /dev/null; }}")
p = subprocess.Popen(["/bin/sh", "-c", cmd], cwd=path, start_new_session=True)
subprocess.Popen(["caffeinate", "-i", "-w", str(p.pid)]) # awake while it lives
deadline = time.time() + timeout
while time.time() < deadline:
if u := url(key): # the link appears in the process output
return u
time.sleep(1)
raise RuntimeError("remote-control did not start")

Details that mattered:

  • The process name holds the state. Each bridge is named tg-<folder>-<hash>, and pgrep tells the bot what’s running. A small JSON registry keeps recent folders, so the menu survives a bot restart.
  • The link comes from the output. The bot waits for claude.ai/code?environment=… to appear and sends it back.
  • The log keeps only the first 64 KB. The Remote Control TUI redraws without end, and the uncapped log grew by about 18 MB a day.
  • The bot refuses some folders and says why. Claude won’t trust the home folder. The permanent bridge already serves its folder, and a second process there breaks its sessions.
  • Only I can use it. The bot checks each update against the owner’s Telegram ID and turns anyone else away.

Layer 3: SSH for everything else

A padlocked tunnel running through a stylised city from a phone to a laptop showing a terminal

Remote Control gives me Claude sessions. Sometimes I need the machine itself.

● mac · tmux: mac
$ ssh me@my-mac          # over Tailscale, key auth
$ tmux attach -t mac
~/Projects/Side/GameJam main
❯ claude
✻ Claude Code
> run the test suite and fix whatever fails
● Bash(npm test)  ⎿ 2 failed, 41 passed
● Edit(src/score.ts)
● Bash(npm test)  ⎿ 43 passed
Fixed an off-by-one in combo scoring. All tests pass.
Termius on the phone, attached to the tmux session launchd keeps open on the Mac. Same shell, same Claude Code, same files.
  • Tailscale puts the phone and the Mac on one private network. Nothing faces the internet, and the Mac keeps the same address wherever I am.
  • Termius is the SSH client on the phone. Auth is key-only: the private key lives in Termius, the public one in ~/.ssh/authorized_keys.
  • tmux starts at login through another launchd agent, so a session called mac is waiting to attach to. A dropped connection on the train leaves running jobs alone.

I chose SSH over a mobile coding app for the reason behind the whole setup: it lands in my environment, with no copy in between.

Four ways it broke

A hand hammering a new plank into a broken bridge next to a toolbox, while a small dog with a lantern guards the laptop at night

The setup ran for weeks before any of these showed up.

1. An update deleted the binary under a running bridge. Claude Code updates itself and removes old versions. A bridge that had run for seven days still pointed at the deleted binary, and each new session failed with posix_spawn ENOENT. A watchdog now runs every five minutes:

~/.local/bin/claude-rc-watchdog (core)
# Kill bridges whose binary was removed by an auto-update.
# launchd restarts mac-Active; tg-* bridges come back from the bot's button.
for p in $(pgrep -f "claude remote-control"); do
exe=$(lsof -a -p "$p" -d txt -Fn 2>/dev/null | grep -m1 '^n' | cut -c2-)
[ -n "$exe" ] && [ ! -e "$exe" ] && kill "$p"
done

2. launchd kept running the old plist. I added --chrome to the plist and nothing changed. launchd caches the job definition, and KeepAlive restarts the cached version. After editing a plist, run launchctl bootout, then bootstrap.

3. Sessions hung on a permission dialog nobody could click. Bridges started by the bot inherit the permissions of the bot’s Python process. The first time a session touched ~/Documents, macOS put up an “allow access” dialog on a screen I wasn’t looking at. Granting Full Disk Access to that Python binary fixed it. The grant is tied to the binary’s signature, so after Homebrew upgrades Python I run a small script over SSH that grants it again.

4. Dead sessions stay dead. When a bridge restarts, its sessions don’t come back (the API answers 409 session_not_active). Open a new session; the files and git state are still there.

What I’d tell you before you build one

  • Keep one machine as the source of truth. Remote access pays off when it leads to the environment you already trust.
  • Let launchd own long-running processes. Anything you start by hand dies with the next reboot.
  • Expect self-updating tools to break long-lived processes. Watch for it with a script.
  • Give each entry point one job. The app for conversations, the bot for “start Claude there”, SSH for the machine itself.

Build your own: paste this into Claude Code

Start Claude Code on the Mac in your home directory and paste:

prompt.md
Help me reach this Mac's Claude Code environment from my phone. Go step by step and ask
before anything that needs sudo or changes system settings.
1. Remote Control as a service: create a launchd agent that runs
`~/.local/bin/claude remote-control --name mac-main --chrome` (absolute paths in the plist)
in my main projects folder, with RunAtLoad and KeepAlive. Load it with launchctl bootstrap.
2. Watchdog: a script that kills `claude remote-control` processes whose binary no longer exists
(lsof txt), plus a launchd agent that runs it every 5 minutes.
3. SSH: check Remote Login, set up key-only auth (I'll give you my public key), a launchd agent
that starts a detached tmux session called `mac` at login, and one that runs `caffeinate -s`.
Remind me to install Tailscale on both devices.
4. Optional: a private Telegram bot (stdlib Python) with one button that lists my project folders,
starts `claude remote-control --name tg-<folder>` in the chosen one, and replies with the
claude.ai/code link from its output. Only my Telegram user ID may use it.