---
title: "Remote agent rig playbook"
date: "2026-06-21"
summary: "The full method an AI agent can follow to set up an always-on machine you reach from any device over mosh, tmux, and Tailscale, with all the gotchas."
download: true
draft: true
---

# Remote Agent Rig Playbook

> **Read this first, if you are the agent.** This is one person's working setup, written down so
> you can rebuild it for someone else. The goal: one always-on machine that runs their coding
> agents, reachable from any device (laptop, phone, tablet, a spare Mac) over a thin, durable,
> low-bandwidth terminal. Treat this as a strong starting point, not gospel. Hosts and operating
> systems differ, so verify every step actually works on the real machine in front of you.
>
> - **Ask the user the questions in section 0 before doing anything.** The right answers change
>   several steps (macOS vs Linux, home vs cloud host, which client devices).
> - **Never hardcode a secret.** Ask the user for their login password when a step needs it, do
>   not embed it. Do not print keys or tokens.
> - **Validate visibly.** After the big steps, have the user actually connect from a second
>   device and confirm the session is there. Do not mark it done off your own exit code.
> - **One profile change is dangerous on the wrong machine.** The always-on, never-lock,
>   auto-login profile in section 1 is fine for a box that lives somewhere trusted. Never apply
>   it to a laptop that leaves the house.

The result you are building, in one line: the expensive work (model calls, installs, builds)
runs on the always-on host over its own connection, and only the text of the screen travels to
whatever device the user is holding. A full day costs tens of megabytes instead of gigabytes,
and the agents keep running whether the user's laptop is open, shut, or in a bag.

---

## 0. Inputs you need from the user

- **The host.** What will stay on all the time? A Mac mini or spare Mac at home, a Linux box, or
  a cloud server (a cheap Hetzner instance works). macOS and Linux both work; some steps differ.
- **Where it lives.** Home LAN or a remote/cloud location. It does not matter for reaching it
  (Tailscale handles that either way), but it decides whether the exit-node VPN in section 12 is
  useful.
- **Client devices.** Which machines will be windows into the host? A Mac laptop, an iPhone or
  iPad, others. iOS devices want Moshi (section 11).
- **Mac terminal.** On a Mac client, use iTerm2 or Ghostty, not the built-in Terminal (section
  11 explains why), and ask whether they prefer a light or dark theme.
- **The agent itself.** Which coding agent runs on the host (Claude Code, Codex, opencode, and
  so on). Install and sign it in there, since that is where it will actually run.
- **Screenshots and notes.** Does the user want to paste images into the agent from a laptop
  (section 9), and do they keep an Obsidian vault to sync (section 10)?

## 1. Make the host behave like a server

The point is that the user never has to be in the room. The host must stay awake and come back
on its own after a reboot or a power cut.

**macOS:**
```bash
sudo pmset -a sleep 0 displaysleep 0 disksleep 0
sudo pmset -a womp 1                  # wake on network
sudo pmset -a autorestartatconnect 1  # boot when AC returns, even from a clean shutdown
```
Then enable auto-login for the host's user so the machine returns to a logged-in session after a
reboot (on macOS the per-user login is also what keeps the standalone Tailscale daemon running).
Set it in System Settings > Users & Groups > Automatically log in, or with
`sudo sysadminctl -autologin set -userName <user>` (ask the user for the password; do not store
it). On recent macOS the CLI sometimes does not regenerate the obfuscated password file, in
which case just toggle auto-login once in System Settings and it sticks. Finally, stop the screen
locking:
```bash
defaults -currentHost write com.apple.screensaver idleTime -int 0
defaults -currentHost write com.apple.screensaver askForPassword -int 0
```

A note on the two power flags: `autorestartatconnect=1` covers strictly more recovery cases than
`autorestart=1` (it also boots after a clean shutdown when AC reconnects). On Apple Silicon they
back the same setting, so set one, not both.

**Linux:** it is already server-shaped. Make sure it never suspends
(`sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target`) and that
your services start at boot.

## 2. Put everything on Tailscale, with no open ports

Tailscale is the private mesh that links the host and every client directly. Nothing is exposed
to the public internet, every device authenticates, and physical location stops mattering.

1. Install Tailscale on the host and on each client, and sign in with the **same account** so
   they all land in one tailnet.
2. Confirm the host appears: `tailscale status` should list it.

**Headless macOS gotchas (skip on Linux):**
- Install the **standalone pkg** from `pkgs.tailscale.com`, not the Mac App Store build. The App
  Store version needs GUI clicks and an Apple ID; the standalone pkg is fully installable over
  SSH.
- Expose the CLI as a **wrapper script**, not a symlink. A symlink to the binary crashes with a
  "bundleIdentifier is unknown to the registry" fatal. Use a one-line `exec` wrapper instead.

Optional: if you serve any web UIs from the host, `tailscale cert <host>.<tailnet>.ts.net` gives
you a real, auto-renewing TLS certificate with no ports forwarded.

## 3. mosh, for a terminal that does not drop

Install mosh on the host and clients. mosh ships only screen diffs (roughly 1 to 3 MB per hour,
versus 100 to 500 MB per hour for VNC or screen sharing) and it survives sleep, wake, and a
switch from wifi to cellular without you noticing.

Gotcha: mosh refuses to start without a UTF-8 locale. Force one in the connect command (see
section 6) or set it in the host's shell startup.

## 4. tmux: persistent sessions, tabs, panes, and the mouse

Install tmux on the host. The session lives **on the host**, not on the device you connect from,
so nothing is tied to the machine in your hand. Everything below is "prefix, then a key", where
the prefix defaults to `Ctrl-b`.

- Detach and leave everything running: `prefix d`.
- Reattach from any device, any time, and you are exactly where you left off.
- Two devices can attach at once; tmux mirrors them, perfect for picking up on a different screen.

**Turn the mouse on. This is the single biggest usability upgrade, so do it.** Add `set -g mouse
on` to `~/.tmux.conf`. Now you can click a tab to switch to it, click a pane to focus it, drag a
pane border to resize, and scroll to move through history. It turns tmux from a keyboard puzzle
into something you drive like a normal app, and for most people it is the difference between
tolerating the terminal and actually enjoying it. When you want to select and copy text with the
mouse instead, hold `Option` in iTerm2 to bypass mouse reporting, or bind a quick toggle (for
example `bind m set -g mouse \; display "mouse #{?mouse,on,off}"`) so `prefix m` flips it off and
back.

**Tabs (tmux calls them windows).** Each tab is a separate workspace. `prefix c` opens a new one,
`prefix ,` renames the current tab (do this, named tabs are far easier to live in than numbers),
and `prefix 1` through `prefix 9`, or a click, jumps between them.

**Panes and layouts.** Split any tab into as many panes as the task wants, arranged however you
like. `prefix %` splits left and right, `prefix "` splits top and bottom, and you repeat to build
a grid. Useful real layouts: a tab with two panes side by side, a tab with three side by side, a
tab with a four-pane grid, plus a handful of dedicated single-pane tabs each doing one job.
`prefix z` zooms a pane to full screen and back. Arrange each window to suit its task; once you
do, the persistence plugins in section 5 remember the exact arrangement.

## 5. Persistence across reboots (tmux-resurrect + tmux-continuum)

A disconnect never touches the host. A reboot kills the tmux server, so add the two plugins that
bring it back:

- Clone `tmux-resurrect` and `tmux-continuum` into `~/.tmux/plugins/` and enable them in
  `~/.tmux.conf`.
- Continuum auto-saves every 15 minutes: window names, order, layouts, working directories, pane
  contents, and it relaunches the programs that were running.
- After a reboot the layout comes back exactly as it was, panes and all.

## 6. One word to reconnect, and a session builder

Put a small launcher on the host whose logic is: **attach if the session is running, else restore
the last saved snapshot, else build a default layout.** Then give each client a shell function so
reconnecting is one word. For example, in the user's `~/.zshrc`:
```bash
work() { mosh <host-on-tailnet> -- ~/.local/bin/worksession "${1:-work}"; }
```
Force the UTF-8 locale here if it is not already guaranteed, since mosh needs it. Now `work`
moshes in and drops the user straight into their live session.

## 7. Bring the session back at boot, unattended

So the rig is live again after a power cut with nobody watching, run the session builder at boot:
- **macOS:** a `launchd` LaunchAgent that runs the builder on login.
- **Linux:** a `systemd --user` service (enable lingering so it starts without a login).

After this, a reboot leaves the host logged in, Tailscale up, and the session rebuilt on its own.

## 8. The tools that live in the panes

The session is far more useful if a few good terminal tools fill those panes. Install these on the
host (Homebrew on macOS, your package manager on Linux):

- **btop** for the monitor pane: a live, colourful view of CPU, memory, and processes, so you can
  watch what an agent's build or test run is doing to the machine at a glance.
- **yazi** for the file-explorer pane: a fast, keyboard-driven file browser with text and code
  previews that work fine over mosh (image previews need plain SSH, since mosh cannot carry
  graphics).
- **lazygit** for reviewing what the agent changed: a visual git UI to stage, diff, commit, and
  push without typing git commands, which is exactly what you want after an agent has been editing
  code. Run it per repo.
- **zoxide** and **fzf** for getting around: `z <name>` jumps to a directory you use often, and
  fzf powers fuzzy history and file search. Small things that add up when you live in the terminal.

A good default session: a tab for each agent (often a multi-pane grid, sized to how big the
screens you work on are), plus a bunch of individual agent tabs for different projects and tests,
one tab running yazi, one for lazygit, and one running btop, or something along those lines.

## 9. Screenshot paste into the remote agent (optional, very worth it)

The problem: mosh cannot carry images, and the agent runs on the host, not on the laptop. The fix
is to do the paste **on the host**, bypassing mosh entirely. Three pieces:

1. **Upload.** A tiny clipboard watcher on the laptop (poll the clipboard every couple of seconds)
   uploads any copied image to an inbox folder on the host, for example `~/inbox/latest.png`. It
   should not alter the laptop clipboard, so the image stays pasteable elsewhere.
2. **A host-side paste binding.** Add a tmux binding that first loads the latest inbox image onto
   the *host's own* clipboard, then triggers a bracketed paste into the focused pane locally, so
   the agent reads the host clipboard and ingests a real image. In `~/.tmux.conf` this is one
   `bind-key` that runs a small script and then sends the paste with `tmux send-keys`. Bound to
   `prefix v`, the whole flow becomes: take a screenshot, wait a second for it to upload, press
   `prefix v`, and the agent gets the actual image.
3. **A one-key shortcut on the Mac (optional).** In iTerm2, map a shortcut like `Cmd-Shift-V` to
   **Send Hex Codes** matching your tmux binding (for `prefix v` that is the hex for `Ctrl-b` then
   `v`), so you do not have to type the prefix. Do not remap plain `Cmd-V`, or it will paste the
   last screenshot instead of text.

Bonus: point the notes-sync in section 10 at the same inbox so your screenshots also land in your
notes automatically.

## 10. Sync notes and your Obsidian vault

- **Code:** git and GitHub are the source of truth. Clone the user's repos onto the host and pull
  to update. Do **not** sync a working tree with a file-syncer; it corrupts `.git`, and
  `node_modules` is huge and architecture-specific. Regenerate `node_modules` on the host with an
  install.
- **Notes / Obsidian vault:** sync this with a two-way file-syncer like Syncthing over the tailnet.
  To set it up: install Syncthing on the host and on each device that holds the vault (laptop,
  phone), add the host and the device to each other, then share the vault folder as a "send and
  receive" folder on both sides. Pin the connection to the tailnet address and you can switch off
  Syncthing's global discovery and relays entirely, so it only ever talks over your private
  network. Edits then appear on both sides within seconds with no commit step, and the same
  Obsidian vault stays identical everywhere. If you point the screenshot upload from section 9 at a
  folder inside that vault, every screenshot the user takes also lands in their notes automatically.

## 11. The clients

- **iPhone / iPad: [Moshi](https://getmoshi.app).** An iOS app built for exactly this, mosh and
  tmux done properly on mobile. Pair by scanning a QR code, sessions reconnect cleanly, and it
  can surface what the agent is doing through notifications and a Live Activity (handy on a
  watch). It makes working from a phone feel real rather than a compromise.
- **Mac: [iTerm2](https://iterm2.com) (or Ghostty), not Terminal.app.** iTerm2 supports
  Command-click to open a link even inside the agent's text UI, Option-drag to select and copy
  text out cleanly, and Send-Hex-Codes key bindings (which power the screenshot paste).
  Terminal.app cannot do these. With `set -g mouse on` (section 4), iTerm2 passes clicks straight
  through to tmux, so you can click tabs and panes; hold `Option` when you want to select text
  instead. Set the look under Settings > Profiles > Colors: iTerm2 ships both light and dark
  colour presets, so pick whichever the user prefers (a light theme all day is a perfectly good
  choice, and the setup this is based on runs light). If you use a tmux status bar, set its
  colours in `~/.tmux.conf` to match.
- **Any other machine.** Anything with SSH or mosh plus a terminal works as a client, with
  nothing special installed on it: a cheap Linux laptop, an old spare, or a borrowed computer
  all attach to the same session. The capable, expensive machine stays at home; the thing in the
  user's hands can be cheap and replaceable.

## 12. Optional: a free VPN back home (exit node)

If the host (or any always-on box on the same tailnet) sits at home, advertise it as a Tailscale
**exit node**. Approve it once in the Tailscale admin console, then toggle it on while travelling
to route all traffic through home: your own trusted IP, home-only services, and your home region,
from anywhere. On macOS an exit node is finicky to run headless; a wired Linux box, router, or
hypervisor host makes a more reliable exit node than the Mac mini itself.

## 13. Reach dev servers from anywhere, and make your agent hand you the right URL

Tailscale does not only carry your terminal. Anything the host serves on a port is reachable from
every device on the tailnet, so a dev server, a staging build, a local database UI, or any
`localhost` app on the host opens from your laptop or phone anywhere in the world, with no tunnel,
no deploy, and no port opened to the public internet. A `localhost` on the host behaves like a
`localhost` in your hand, so you review work in progress from a train the same way you would at
your desk.

Two things make this smooth, and both are worth baking into the agent's standing instructions
(its global prompt or rules file) so you never have to ask:

1. **Bind dev servers to all interfaces, not just localhost.** A server bound to `127.0.0.1` is
   only reachable on the host itself. Bind it to `0.0.0.0` (for example `next dev -H 0.0.0.0`,
   `vite --host 0.0.0.0`) so it answers over the tailnet.
2. **Report the Tailscale URL, never localhost.** Tell the agent that whenever it starts a server
   it must hand back `http://<host>.<tailnet>.ts.net:<port>/`, not `http://localhost:<port>`, so
   the link is one the user can actually click from whatever device they are on.

A single line like "any dev server you start must bind to 0.0.0.0, and you must give me its
Tailscale URL, not localhost" in the agent's instructions turns every preview link into something
openable from anywhere. After that, working on the remote host feels exactly like working locally.

## 14. Validate end to end

- **Mirror test:** attach the same session from a second device and confirm it shows the same
  thing live.
- **Reboot test:** reboot the host and confirm it auto-logs-in, Tailscale comes up, and the
  session is rebuilt on its own.
- **Bandwidth test:** run a long session and confirm the data used is tens of megabytes, not
  gigabytes. If it is large, something is pixel-streaming instead of sending text; fix that.

## 15. Tmux quick tips (the daily-driver cheat sheet)

Everything is "prefix, then key", and the prefix defaults to `Ctrl-b`. These are the ones worth
knowing by heart:

| Do this | Press |
|---|---|
| Detach, leave everything running | `prefix d` |
| New tab | `prefix c` |
| Rename the current tab | `prefix ,` |
| Switch tab | `prefix 1`...`9`, or click it |
| List / pick tabs | `prefix w` |
| Split into two panes, left and right | `prefix %` |
| Split into two panes, top and bottom | `prefix "` |
| Move between panes | `prefix` then an arrow key, or click |
| Zoom a pane full screen and back | `prefix z` |
| Toggle the mouse on or off | `prefix m` (once bound, see section 4) |
| Paste the latest screenshot into the agent | `prefix v` (the binding from section 9) |
| Scroll back through output | scroll with the mouse, or `prefix [`, then `q` to quit |

If a shortcut ever feels stuck, you are probably in scroll or copy mode: press `q` or `Esc` to get
back to normal.

---

# GOTCHAS, read this twice

**A per-app network filter on the laptop blocks every new app.** Tools like TripMode silently
block any app they have not seen: the terminal, the file-syncer, mosh, even git. The symptom is a
connection that just fails, sometimes `mosh: Bad file descriptor`. Fix: allow the app in the
filter. Suspect this first whenever something new will not talk over the tailnet from the laptop.

**The host's own OS firewall can block inbound to a new app, and the prompt is invisible over
SSH.** Approve it once at the physical screen, or over screen sharing.

**Tailscale can die on some roaming carriers.** A few mobile networks break IPv6 to Tailscale's
control plane. Make sure IPv4 works so it can fall back, and the node recovers.

**mosh cannot carry images directly.** That is the entire reason for the on-host paste trick in
section 9. Do not try to pipe an image over the mosh link.

**Mouse mode changes how copy works.** With `set -g mouse on`, a normal click-drag selects into
tmux's own copy mode, not your terminal's clipboard. To get a real system copy, hold `Option` in
iTerm2 while you drag, or toggle mouse mode off with `prefix m` first.

**Non-interactive shells do not read your interactive config.** The shell that runs
`ssh host 'cmd'`, and the one launchd or systemd use at boot, is non-interactive and non-login. It
does not source `~/.zshrc`, so anything you added there (a Homebrew PATH, for instance) is missing,
and boot scripts fail to find your tools. Put PATH and environment setup in the file every shell
sources (`~/.zshenv` for zsh), not only in `~/.zshrc`.

**A passphrase-protected SSH key is invisible to automation.** A key locked in the macOS Keychain
works for your interactive logins but not for `ssh host 'cmd'` or boot scripts (no agent socket in
those sessions). Use an unencrypted key for the automation and boot paths, or run that work inside
an already-interactive session.

**Never put the server profile on a travelling laptop.** Disabling sleep, the lock screen, and the
login password, plus auto-login, is fine for a machine that stays somewhere trusted behind the
tailnet. It is the wrong thing entirely for a laptop that leaves the house.

**Match old habits to the new shape.** Detaching (`prefix d`) leaves everything running; that is
the normal way to "leave". Closing the terminal app or losing signal does the same thing. The only
event that needs the persistence plugins is a full reboot of the host.
