Link two Macs over your own Tailscale network and reconcile them after a trip. Guided setup mints a dedicated key, installs it password-only, and writes the SSH config; then a read-only drift report compares every git repo, unpushed branch, uncommitted change and Claude Code session across both machines. Never merges, resets or copies secrets on its own.
macsshtailscaleremotegitworktreeclaude-codesync
Mac Sync is a Claude Code skill. Link two Macs over your own Tailscale network and reconcile them after a trip. It works well with cmux-setup.
---
name: mac-sync
description: "Link two Macs over your own Tailscale network and keep them reconciled. Guided setup mints a dedicated SSH key, installs it password-only, writes the ~/.ssh/config block, and verifies the link; then a read-only drift report compares every git repo, unpushed branch, uncommitted change and Claude Code session across both machines so nothing is lost after a trip. Ships a link health-check, a reconcile reporter, and a dry-run-by-default push helper. Use when the user says \"mac-sync\", \"connect my Macs\", \"link my laptop and desktop\", \"work on my other Mac\", \"remote Mac\", \"what changed on my laptop\", \"sync after a trip\", or asks to read or run something on their second Mac."
invoke: user
---
# Mac Sync
Two Macs means two sources of truth. You commit on the laptop, forget to push,
and rebuild the same fix on the desktop a week later. Or you reset a branch to
tidy it and take real work with it.
This skill closes that gap in two halves:
1. **Link** the two machines once, over Tailscale, with a dedicated key.
2. **Reconcile** them whenever you come back — a read-only report of everything
that drifted, so you decide per project instead of guessing.
It reads by default. It never merges, resets, force-pushes, or copies secrets
on its own.
---
## Step 0 — Prerequisites (BLOCKING)
Before any other operation, verify these. If any are missing, **STOP** and tell
the user where to get each one. Do NOT generate placeholder bash and do NOT
proceed with broken state.
| # | Requirement | Check | If missing |
|---|---|---|---|
| 1 | Tailscale installed and logged in, on **both** Macs | `tailscale status` lists both machines | https://tailscale.com/download — install and sign in with the same account on both |
| 2 | `ssh` client (ships with macOS) | `ssh -V` | Reinstall Xcode command line tools: `xcode-select --install` |
| 3 | **Remote Login enabled on the target Mac** | On the target: System Settings → General → Sharing → Remote Login = on | GUI-only. Ask the user to switch it on; it cannot be enabled over the network. |
| 4 | The target account is an **admin** on that Mac | On the target: `id -Gn <user> \| tr ' ' '\n' \| grep -x admin` | macOS gates Remote Login behind the `com.apple.access_ssh` group, which nests to `admin`. A standard account is refused even with a correct password AND a valid key. Either grant admin, or add the account to Remote Login's allowed-users list in the Sharing pane. |
| 5 | `git` ≥ 2.30 (only for the reconcile half) | `git --version` | `brew install git` |
If anything is missing, STOP.
Two things worth saying out loud before you start, because they surprise people:
- Access rides Tailscale, so **the target Mac is never exposed to the public
internet**. If both nodes are owned by the same Tailscale account, the default
tailnet policy already permits this and **no ACL edit is needed** — do not add
one. If access breaks later, check `tailscale status` before touching policy.
- **Leave password auth enabled.** It is tempting to harden it off once keys
work. Don't: Tailscale is already the network perimeter, and keys-only means
a lost key costs you a physical trip to the machine. Password auth is the
recovery path.
---
## Step 1 — Interview
Ask before writing anything. Use `AskUserQuestion` when it is available. Nothing
in this skill is hardcoded; every value below comes from these answers.
1. **What should the connection be called?** A short lowercase alias the user
will type — `laptop`, `desktop`, `studio`. This becomes the `Host` name, so
`ssh laptop` is the whole command.
2. **Which machine is the target?** Take the Tailscale IP or the MagicDNS name
from `tailscale status`. Prefer the **MagicDNS name** if the tailnet has it
enabled — it survives an IP change.
3. **What is the account name on that Mac?** Usually the short username; confirm
with `whoami` on the target rather than guessing from the machine name.
4. **Is this a peer Mac or an always-on box?** A laptop that sleeps is fine as a
peer. An always-on machine you expect to reach at any hour needs Step 4.
Confirm the four answers back to the user before writing to `~/.ssh/config`.
---
## Step 2 — Mint a key and install it
Make a key **dedicated to this link**, not a reused general-purpose key. A
dedicated key is revocable on its own: you can cut this one connection without
breaking every other thing that key opens.
```bash
ssh-keygen -t ed25519 -f ~/.ssh/<alias>_key -C "<this-machine>-><alias>" -N ""
```
Install it. The flags are not optional:
```bash
ssh-copy-id -o PubkeyAuthentication=no \
-o PreferredAuthentications=password,keyboard-interactive \
-i ~/.ssh/<alias>_key.pub <user>@<host>
```
**Why the flags:** `ssh-copy-id` bypasses your `Host` alias, so `IdentitiesOnly`
does not apply and ssh offers every key in `~/.ssh` first. That exhausts the
server's auth-attempt budget and disconnects you after one or two password
tries — it looks like a wrong password when it is not. Forcing password-only
auth for this one command avoids it.
If the password is not to hand, install the key directly in Terminal **on the
target Mac** instead:
```bash
mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo '<paste the .pub contents>' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
```
Then write the config block (append to `~/.ssh/config`, create it `chmod 600` if
absent):
```
Host <alias>
HostName <tailscale-ip-or-magicdns-name>
User <user>
IdentityFile ~/.ssh/<alias>_key
IdentitiesOnly yes
StrictHostKeyChecking accept-new
ServerAliveInterval 30
```
`IdentitiesOnly yes` stops ssh offering unrelated keys on every connect.
`accept-new` trusts the key the first time and then pins it — so a **changed**
host key later is a real warning. If that happens, stop and tell the user; do
not clear `known_hosts` to make it go away.
---
## Step 3 — Verify the link
```bash
~/.claude/skills/mac-sync/scripts/mac-link-check.sh <alias>
```
That script is read-only. It checks reachability, confirms a login shell finds
the user's real toolchain, reports the power settings, and flags the two macOS
permission traps. Run it now, and any time the link misbehaves later.
The one check to understand, because you will hit it constantly:
```bash
ssh -o BatchMode=yes <alias> 'bash -lc "node -v"' # works
ssh -o BatchMode=yes <alias> 'node -v' # command not found
```
Always pass `-o BatchMode=yes` so a broken link fails fast instead of hanging on
an interactive prompt.
---
## Step 4 — Keep an always-on Mac reachable
Skip for a laptop. For a machine you expect to reach at any hour:
```bash
ssh -o BatchMode=yes <alias> 'pmset -g | egrep "sleep|womp|autorestart"'
```
Want `sleep 0` (never fully sleeps), `womp 1` (wakes on network), and
`autorestart 1` (comes back after a power blip), on AC power. `disksleep` is
harmless — disk sleep does not drop the tailnet. Only if those regress:
```bash
ssh -o BatchMode=yes <alias> 'sudo pmset -a sleep 0 womp 1 autorestart 1'
```
A sleeping Mac is an unreachable Mac, and it reads exactly like a broken
connection. Check power before debugging ssh.
---
## Step 5 — Reconcile after a trip
This is the half that earns the skill. Full playbook, including the decision
table for every repo state: **`references/post-trip-reconcile.md`** — read it
before acting on a report.
```bash
# same project root on both Macs
~/.claude/skills/mac-sync/scripts/mac-reconcile.sh <alias> ~/path/to/projects
# different roots (common) — pass both, or one side reports nothing
~/.claude/skills/mac-sync/scripts/mac-reconcile.sh <alias> ~/code --remote-dir '~/Projects'
# add --sessions to also compare Claude Code transcripts
```
Read-only. For every git repo on both machines it reports the branch, how far
ahead or behind `origin` it is, uncommitted and staged files, stashes, and
registered worktrees — then diffs the two lists so you can see what exists on
one machine and not the other. It also lists Claude Code session transcripts
(`~/.claude/projects/`) present on only one side.
Then work the report **one project at a time**. The rules that matter:
- **A divergence gets a rescue branch, never a reset.** If both machines have
commits the other lacks, push the far side to a throwaway branch and merge
deliberately. `git reset --hard` on a diverged branch is how the work
disappears.
- **Uncommitted work is the fragile thing.** Commit it (even to a WIP branch) or
stash it before anything else moves. It is the only state with no remote copy.
- **Never sync `.env*`, credentials, key material, `node_modules` or build
caches.** This is a rule, not a default — say so when you propose a copy.
To actually move files once the user has chosen:
```bash
~/.claude/skills/mac-sync/scripts/mac-push.sh <alias> <local-src> <remote-dest> # preview
~/.claude/skills/mac-sync/scripts/mac-push.sh <alias> <local-src> <remote-dest> --go # apply
```
Dry run is the default; `--go` writes. `--delete` is opt-in, never implied, and
requires `--go`. The script refuses an empty destination, `/`, `~`, and `.`.
**Trailing slash on the source matters** (rsync semantics): `foo` copies the
folder to `dest/foo/`; `foo/` copies its *contents* into `dest/`.
---
## Operating protocol
Once the link works, these are the things that will otherwise cost you an
afternoon each. Symptom, cause and fix for all six:
**`references/gotchas.md`**. The short version:
- **Wrap anything needing real tooling in a login shell.** A plain
`ssh <alias> '<cmd>'` runs with a stunted PATH — no Homebrew, no node, no
`gh`. Use `bash -lc "..."`.
- **Use SINGLE quotes around remote commands.** `ssh <alias> "~/foo"` expands
`~` on *your* machine and sends an absolute path that does not exist on the
other one. Single quotes let `~` expand remotely. This bites every `~` path,
not just the obvious ones.
- **Quote paths with spaces INSIDE the command string.** The remote shell
word-splits them even when your local side is quoted:
`ssh <alias> 'ls -d ~/"My Projects"'`.
- **Environment set through the macOS GUI layer is invisible over SSH.** That is
a macOS boundary, not something to debug.
- **A locked login keychain fakes a logged-out CLI.** `gh auth status` and
similar can report missing credentials over SSH while being perfectly healthy
locally. **Do not re-authenticate** — you will clobber working credentials.
- **`launchctl` needs the GUI domain to act.** A bare `launchctl list` works over
SSH, but `print`, `kickstart`, `bootout` and `bootstrap` need
`gui/$(id -u)/<label>`. And never use `kickstart` as a connectivity test — it
fires the real job.
### Safety rules
- **Confirm before writing, deleting, killing processes, or running installers
on the other Mac.** It is a machine someone uses; reading and reporting is
free, mutation is not.
- **Never print secrets.** `.env` files and credential stores are readable over
this link. Read a specific key with `grep` if you must; never `cat` a whole
env file into output, and never echo a value into a summary, a log, a commit,
or a chat message.
- **Check free disk space on the target before anything that creates large
files.**
- **If the remote copy of a file is newer than yours, say so and stop.** The
other machine may have been used since. A blind push silently destroys work.
---
## Revoking access
Remove the key line from the target's `authorized_keys`, then delete the local
key:
```bash
ssh <alias> "grep -v '<the key comment>' ~/.ssh/authorized_keys > /tmp/ak && mv /tmp/ak ~/.ssh/authorized_keys"
rm ~/.ssh/<alias>_key ~/.ssh/<alias>_key.pub
```
Password auth remains as the way back in — which is exactly why Step 0 said to
leave it enabled.
**The private key never leaves `~/.ssh/` and is never committed to any repo.**
---
## Composes well
`cmux-setup` — if you run agents in parallel across both machines, set up the
workspace side there and the link here.
JF
Built by Jay Feldman
Founder, Lead Gen Jay · Inc. 5000 · 84K+ YouTube subs