---
title: "Two MacBooks, One Claude Code Setup"
description: "How I wired a home-server MacBook and my daily machine so I can SSH in from anywhere and pick up exactly where Claude Code left off, and the small mistakes that made it take longer than it should have."
date: 2026-07-27
category: Engineering
readingTime: "3 min read"
---


I run two Macs. One is the daily machine, sitting on my desk, doing the actual work. The other stays on at home around the clock: power, Wi-Fi, the TV, an old MacBook Air repurposed as an always-on box. They are bridged over Tailscale, so either one can reach the other from anywhere without touching router configuration or opening a port to the internet.

The daily machine is where every codebase lives, and where Claude Code has spent months accumulating configuration: agents, skills, hooks, memory. I wanted to sit down at the home server, SSH in from my phone or a laptop that is not mine, and get the same Claude Code, the same conventions, working on the same code, without a second copy of anything drifting out of sync.

## What actually needed copying

Claude Code's configuration lives almost entirely under `~/.claude`, and most of it is portable: instructions, agent definitions, hooks, skills, a plugin marketplace cache, an auto-memory folder built up over many sessions. None of it is tied to a specific machine.

Some of it very much is. The credential store, the machine identity, the list of registered projects, several hundred megabytes of disposable plugin-cache clones that regenerate on demand. Copying blindly would have dragged over a gigabyte of junk and, worse, machine identifiers that should never be duplicated.

So the rule was: sync the parts that are configuration, leave the parts that are state. `rsync` for the agents, skills, hooks, and memory directories; a deliberate skip on the plugin cache; a merge, not an overwrite, for the two JSON files that mix portable settings with machine-specific fields. One plugin needed a manual fix carried over by hand, a MongoDB MCP server that fails to connect out of the box and gets neutralised by emptying its manifest, a workaround from months back that only lives in the plugin cache and so never gets picked up by a plain file copy.

## The part that actually took the time

None of the above was the hard part. The hard part was authentication, and it took the time because I kept reaching for the wrong tool.

Claude Code normally stores its session credential in the macOS Keychain. That is fine at the console, in front of the screen, where the OS can show you an unlock prompt if it needs one. It is not fine over SSH. A headless shell has no window server to show that prompt in, so any command that touches the Keychain just hangs, waiting forever for a dialog nobody will ever see. `claude doctor` names the problem outright if you ask it to, but the instinct when you hit a locked Keychain is to unlock it, and that instinct is a dead end here: there is no Keychain state that persists cleanly across a fresh, non-interactive SSH session the way it does across a real login.

The actual fix already existed and I nearly missed it: `claude setup-token`. It runs the same browser approval flow as a normal login, but instead of writing to the Keychain it prints a long-lived OAuth token to the terminal, one you export as an environment variable. No Keychain, no locked session, nothing that depends on whether a human is sitting at the screen. It is built for exactly this: CI pipelines, scripts, anywhere a browser can approve once but nobody will be present for every subsequent run.

Once that was wired in, the rest fell into place quickly. A small shell function on the home server does the whole hop in one word: SSH into it, forward that OAuth token onward, land in the right directory on the daily machine, launch Claude Code there. The account, the files, and the working directory all live on the daily machine; the home server is only ever a relay you pass through to get there.

## The lesson that mattered more than the setup

Along the way I made the same mistake twice: pasting a live credential into a chat transcript to save a few seconds of typing it directly at a terminal. It is a completely understandable shortcut, and it is also exactly the failure mode credential handling is supposed to prevent. A transcript gets logged. A dotfile gets synced. A token typed where it should not have been typed is a token that should be treated as burned, not a token that is merely inconvenient. The fix cost nothing, generate a new one, type it only where it is meant to live, and that is the whole point: the safe path and the fast path are usually the same number of keystrokes, and it is only under a little bit of hurry that the difference feels worth skipping.

The setup itself is now almost invisible. I SSH into the home server, run one command, and I am working on the real codebase on the real machine, as if I had walked over and sat down at it. That invisibility was the actual goal all along. The interesting engineering was never the two Macs or the SSH hop, it was making sure the seam between them disappeared.
