Remote Claude

My Mac runs all my Claude sessions: chats in T3, agents in tmux, a few things in Zellij. When I close the lid and leave, I still want every one of them on my phone. The Travel Setup page covers the terminal side. This is the rest of it.

Keeping a closed Mac awake

Closing the lid is its own sleep trigger. caffeinate and power assertions keep an open Mac awake, but they can't stop lid-close sleep. The only switch that does is pmset disablesleep.

Leaving that on forever is how a MacBook cooks in a bag. So a LaunchAgent flips it every 30 seconds. It stays awake only if something is worth staying awake for (a live tmux or Zellij session, or the T3 Connect tunnel), and only on the charger or above 15% battery. Otherwise it's a normal Mac again.

# travel-lid-guard, run by a LaunchAgent every 30s
live_sessions() {
  z=$(zellij list-sessions -n | grep -v EXITED | grep -c .)
  t=$(tmux ls 2>/dev/null | grep -c .)
  # T3 counts while its Connect tunnel is up
  pgrep -qf "$HOME/.t3/tools/cloudflared/.*tunnel run" && t3=1
  print $(( z + t + t3 ))
}

should_stay_awake() {          # sessions, power source, battery %
  (( $1 > 0 )) || return 1     # nothing running: sleep like a normal Mac
  [[ $2 == AC ]] && return 0
  (( $3 > 15 ))                # on battery, give up below 15%
}

# on:  sudo -n pmset -a disablesleep 1
# off: sudo -n pmset -a disablesleep 0

disablesleep also keeps the internal panel lit behind the closed lid, so the guard runs pmset displaysleepnow whenever it sees the lid shut. The sudo rule is NOPASSWD for /usr/bin/pmset and nothing else.

T3 Code on the phone

My Claude chats live in T3 Code, a desktop app that drives Claude Code and Codex. T3 Connect opens an outbound Cloudflare tunnel from the Mac, and T3 runs cloudflareditself. The phone app signs in with the same account, I match a code, approve on the Mac, and every thread shows up. There's no port forwarding, and it works on mobile data too.

From the phone I can continue a chat, start a new one, approve permission prompts and read diffs. Nothing has to be running beforehand, because T3 starts Claude on demand. That's why the lid guard counts the tunnel and not busy agents: if it waited for an active agent, the Mac would sleep and I couldn't reach it to start one.

One trap: I linked the Mac under one account, logged out, and signed in with another. Logging out doesn't unlink the machine, and the toggle in the app only clears the local copy. The relay kept the old tunnel. The CLI bundled in the app fixed it:

# T3 ships its own CLI inside the app bundle
$ t3 connect status
  Exposure:          enabled
  Authorization:     stored credential
  Environment link:  pending server startup

# switching accounts: logout alone leaves the old link behind
$ t3 connect login      # browser sign-in with the new account
$ t3 connect unlink     # clears the relay's record, not just the local one
$ t3 connect link       # then fully restart T3 (Cmd+Q, reopen)

Same keys in every chat

Half my API keys worked in my terminal and not in agents. Agents run commands as zsh -c, which reads ~/.zshenv and skips ~/.zshrc, where most of my keys were. Moving the exports fixed every tool at once. Each key sits in its own 600 file, so the rc file never holds a secret.

# ~/.zshenv is read by every zsh, including the "zsh -c" that
# agents run. ~/.zshrc is interactive only, so keys there are invisible.
[ -f ~/.linear_api_key ]  && export LINEAR_API_KEY="$(cat ~/.linear_api_key)"
[ -f ~/.posthog_api_key ] && export POSTHOG_API_KEY="$(cat ~/.posthog_api_key)"
[ -f ~/.config/fireflies/env ] && source ~/.config/fireflies/env

A global ~/.claude/CLAUDE.mdtells every session where things live and which CLIs are logged in, by path and env var name only. New chats don't start by asking me for a key.

Letting agents use my real Chrome

Agents use my actual logged-in Chrome through chrome-devtools-mcp and chrome://inspectremote debugging. Chrome asks "Allow remote debugging?" for new connections, and the approval only lasts a few minutes. Fine at a desk, useless with the lid closed. There's no setting to remember it.

So I wrote a small Swift LaunchAgent. When the prompt shows up, it checks who is connected to Chrome's debug port with lsof. It presses Allow only if every client is chrome-devtools-mcp with a claude process above it, and one of them connected in the last 30 seconds. Anything else is left for me. Leftover prompts get Cancel, and it never looks inside web pages, so a site's own "Allow" button can't be hit.

I'm still a little uneasy about a program that clicks a security prompt for me. That's why the rules are this strict: the prompt still does its job for everything except Claude's own browser tool.

# ~/Library/Logs/chrome-debug-autoallow.log
ALLOW  chrome-devtools-mcp <- npm exec <- claude <- tmux
ALLOW  chrome-devtools-mcp <- npm exec <- claude <- T3
SKIP   node /tmp/probe.mjs [node <- zsh <- claude <- T3]   # left for me

Things that bit me while building it:

  • The prompt is drawn inside a browser window, and Accessibility only sees windows on the current Space. Chrome is usually on another one, so it borrows the private API AltTab uses.
  • On macOS 27 a LaunchAgent can't read Chrome's profile folder, so the port comes from Chrome's listening socket instead of DevToolsActivePort.
  • Foundation's Process.waitUntilExit() sometimes never returned and froze the helper for good. Plain posix_spawn and waitpid fixed it.
  • Ad-hoc signing ties the Accessibility grant to the exact binary, so every rebuild meant re-adding it. A self-signed certificate keeps the grant across rebuilds.

Still on my list: confirming it can press buttons while the screen is locked.

Daily flow

  1. Mac on the charger, T3 open with T3 Connect on, Chrome with remote debugging on.
  2. Close the lid. The guard sees the tunnel, turns off sleep and blanks the screen.
  3. On the phone: open T3, pick a chat or start one. It has my keys and my browser.
  4. Unplug with nothing running and the Mac sleeps like any other.