What's in here
- The shape of the stack
- Base system: Homebrew, Node, Git
- Secrets first — before anything starts
- Clone the brain repos
- Install the primary agent runtime
- Install the secondary runtime + profiles
- Reconnect the MCP servers
- The Obsidian vault (and the symlink trick)
- launchd: making it survive reboots
- Backups: git for text, encrypted archive for state
- Scheduled jobs
- Verification checklist
- Gotchas that cost me a night each
- Driving all of this with an agent
01The shape of the stack
Before touching a terminal, it helps to know what you're actually rebuilding. Mine is four layers, and they fail independently — which is the whole reason the restore order matters.
| Layer | What it is | Where it lives |
|---|---|---|
| Runtime | The agent gateway process. Holds sessions, routes messages, executes tools. | ~/.openclaw |
| Personas | A second runtime running named profiles — one long-running agent per business function. | ~/.hermes |
| Memory | An Obsidian vault. Plain markdown. Daily logs, decision logs, shared context. | ~/obsidian/<vault> |
| Supervision | launchd agents that keep everything alive, plus cron jobs and backup scripts. | ~/Library/LaunchAgents |
Two things are worth internalizing because they drive every decision below.
Config is text, state is binary. Config — JSON, YAML, markdown, skills, prompts — goes in git and restores in seconds. State is SQLite: conversation history, session mapping, task queues. Mine runs into the gigabytes and blows straight past GitHub's 100 MB file limit, so it cannot live in the same place. That split is why there are two backup systems in section 10, not one.
Nothing here is a container. Everything is a native process supervised by launchd. That's a deliberate trade: I lose reproducibility, I gain a Mac that can drive a real browser session, read the keychain, and hold OAuth logins that would be miserable inside Docker.
02Base system
Install Homebrew first — on Apple Silicon it lands in /opt/homebrew, and a lot of the
paths later depend on that being true.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
Then the packages that actually matter to the stack:
brew install node git gh ripgrep poppler pnpm
# poppler gives you pdftoppm — you will want it the moment an agent
# generates a PDF and you need to show someone the result as an image
Optional but I use all of them regularly:
brew install tesseract openai-whisper yt-dlp cmake vim go
Write down the Node major version the old machine was running and match it. Agent runtimes tend to
ship native modules, and a silent major-version jump is a genuinely miserable first thing to debug
on a fresh box. node --version on the old Mac before you wipe it.
Git identity and GitHub auth
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
ssh-keygen -t ed25519 -C "newmac-$(date +%Y%m%d)"
pbcopy < ~/.ssh/id_ed25519.pub # paste into GitHub → Settings → SSH keys
ssh -T git@github.com # confirm before moving on
It is very tempting to clone with
https://x-access-token:ghp_…@github.com/… because it just works with no agent, no
passphrase, no prompt. Don't. That token is then sitting in plaintext in
.git/config in every repo, which means it leaks through screenshots, screen shares,
and pasted error output. SSH keys or the macOS keychain credential helper — pick either, just not
the URL.
If you're reading this on an existing machine and you already did it, the migration is mechanical. Audit first, then rewrite the remotes:
# find every repo whose remote has a credential baked into the URL
grep -rl "x-access-token\|ghp_\|github_pat_" ~ --include=config 2>/dev/null
# rewrite one repo from https-with-token to ssh
git remote set-url origin git@github.com:<you>/<repo>.git
git remote -v # confirm no secret left in the URL
git fetch origin # prove the new auth path works
# or keep https and let the keychain hold it instead
git config --global credential.helper osxkeychain
Changing the URL removes the token from disk going forward, but the token itself has already been written into files, backups and archives. It is still valid until you revoke it. Rewrite the remotes, then revoke the old token in GitHub → Settings → Developer settings and issue a new one. Do it in that order — revoking first breaks every automation still using it, and you will find out which ones the hard way.
03Secrets first — before anything starts
This is the step people do last and regret. If the gateway starts before its credentials exist, it boots into a broken auth state, launchd sees it exit, restarts it, and you spend forty minutes reading crash logs that describe a problem you already know how to fix.
Restore secrets before installing a single service.
Password manager CLI
npm install -g @bitwarden/cli
bw login
export BW_SESSION=$(bw unlock --raw)
The credential directory
I keep every API key, OAuth client, and refresh token as an individual file under a single directory, which makes both backup and audit trivial:
~/.openclaw/secrets/
├── gmail/ # per-inbox OAuth token files
├── google-oauth-client.json
├── crm-<tenant>.json # one per CRM tenant
├── notion-token
└── <service>-oauth.json # refresh tokens that rotate
mkdir -p ~/.openclaw/secrets && chmod 700 ~/.openclaw/secrets
Keychain items
Anything a background script needs unattended goes in the login keychain, not in a dotfile. My encrypted-backup passphrase is the main one — the backup script reads it at 3am with no human present:
security add-generic-password -a "$USER" -s "agent-dr-backup-key" -w
# prompts for the value instead of putting it in shell history
A machine migration is the cheapest possible moment to rotate credentials, because you are already going to re-authenticate everything. If a key has ever been pasted into a chat, a log, or a git remote, replace it now rather than faithfully restoring a known leak.
04Clone the brain repos
Every agent's config directory is its own private git repo. That is the actual backup — a commit per day, pushed to GitHub, with full history. Restoring is a clone.
mkdir -p ~/obsidian
git clone git@github.com:<you>/agent-primary.git ~/.openclaw/workspace
git clone git@github.com:<you>/agent-secondary.git ~/.hermes
git clone git@github.com:<you>/vault.git ~/obsidian/<vault>
# nested repos that are versioned separately
git clone git@github.com:<you>/profile-a.git ~/.hermes/profiles/a
git clone git@github.com:<you>/profile-b.git ~/.hermes/profiles/b
These repos contain system prompts, business context, customer-facing policy, and internal decisions. Mine are all private and I check that periodically rather than assuming — a repo that was created private can be flipped later, and nothing warns you.
What gets excluded
The .gitignore in each brain repo is doing real work. Mine excludes three categories:
# bloat — regeneratable, multi-GB
logs/
cache/
tmp/
sandboxes/
# transient runtime
*.log
*.pid
*.lock
*.db-wal
*.db-shm
gateway_state.json
# nested repos with their own backup
profiles/
Notice what is not in there for the primary workspace: *.db itself is kept,
because a small state file is worth versioning. For the secondary runtime, whose
state.db is far larger, it is excluded — and that gap is covered by the
encrypted archive in section 10. Know which of your databases fall on which side of that line
before you trust the backup.
05Install the primary agent runtime
npm install -g openclaw
openclaw --version
The workspace you cloned in the previous step is the agent's brain — persona files, tool docs, agent roster, memory index. The machine-level config is a separate file that is not in that repo, because it holds channel tokens:
~/.openclaw/
├── openclaw.json # machine config — channels, models, MCP, auth
├── workspace/ # ← the git repo you just cloned
├── secrets/ # ← restored in section 3
├── scripts/ # backup + healthcheck shell scripts
└── agents/ # per-agent runtime state
Restore openclaw.json from your encrypted archive (section 10) or rebuild it with the
setup wizard. The top-level keys worth knowing:
| Key | What it controls |
|---|---|
| agents | Agent list, per-agent workspace, persona, model, fallbacks |
| channels | Chat platform connections — bot tokens, allowlists, per-channel behavior |
| mcp | External tool servers (section 7) |
| models | Provider routing and fallback order |
| tools | What the agent is allowed to call; alsoAllow extends, allow restricts |
| memory | Memory index configuration |
Configure at least two fallback models from different providers. When a provider has an auth hiccup, an agent with no fallback doesn't error visibly — it just goes silent in your chat channel, and you find out hours later when you notice you haven't heard from it. A fallback turns a silent outage into a slightly-worse answer.
06Install the secondary runtime + profiles
My second runtime is a Python agent framework, cloned from source and run out of a virtualenv. Each named profile is a separate long-running agent with its own config, skills, state database, and chat bot identity — so one can answer guest messages while another watches store operations, without sharing context.
git clone https://github.com/NousResearch/hermes-agent.git ~/.hermes/hermes-agent
cd ~/.hermes/hermes-agent
python3 -m venv venv
./venv/bin/pip install -e .
The launcher shim
Do not symlink the venv binary directly onto your PATH. If you have any global Python environment variables set — and if you've ever installed a data science tool, you do — they leak into the venv and it imports the wrong packages. A three-line wrapper solves it permanently:
mkdir -p ~/.local/bin
cat > ~/.local/bin/agentcli <<'EOF'
#!/usr/bin/env bash
unset PYTHONPATH
unset PYTHONHOME
exec "$HOME/.hermes/hermes-agent/venv/bin/hermes" "$@"
EOF
chmod +x ~/.local/bin/agentcli
Profile layout
~/.hermes/
├── config.yaml # default profile
├── auth.json # provider tokens
├── skills/ # reusable procedures
├── state.db # ← big, gitignored
└── profiles/
├── a/ # own config.yaml, auth.json, skills/, state.db
└── b/
A profile gateway reads its own profiles/<name>/auth.json. Running the
auth command from the default context writes to the top-level file, which the profile gateway will
never look at — so you get an agent that reports healthy and fails every single model call. If one
profile is mysteriously the only broken one, check whether it has its own token before you debug
anything else.
07Reconnect the MCP servers
MCP (Model Context Protocol) servers are how agents touch real systems. I run about seventeen —
mail, calendar, drive, accounting, CRM, PMS, home automation, plus internal business APIs. They're
declared in openclaw.json under mcp, and they come in two flavors.
stdio servers run as a local subprocess and speak newline-delimited JSON over stdin/stdout. HTTP/SSE servers are remote endpoints, usually with a bearer token and often OAuth.
npm install -g mcporter
mcporter list # what's configured
mcporter test <server-name> # does it actually answer
Always verify an MCP server responds before restarting the gateway to pick it up. A malformed MCP entry can prevent the whole config from loading, and the failure mode is the gateway quietly refusing the new config and continuing on the old one — so your change appears to do nothing rather than appearing to break.
OAuth servers need a re-consent pass
Anything using OAuth needs re-authorization on a new machine. Two things to check while you're in there:
- Publish your OAuth consent screen. A Google Cloud project left in "Testing" issues refresh tokens that expire every 7 days, forever. You will rediscover this every single week until you publish it to production. Ask me how I know.
- Automate rotation for tokens that rotate. Some providers invalidate the old refresh token every time you use it. If you manually curl the refresh endpoint to test, you have just burned the token your automation was going to use. Put the refresh on a schedule and then leave it alone.
08The Obsidian vault
The vault is the part people underestimate. It's just markdown files in a git repo, and it is the single highest-leverage piece of the entire stack — because it's the only layer where the agent's memory is legible to me. I can read it, edit it, and correct it without touching a database.
~/obsidian/<vault>/
├── Agent-Shared/ # cross-agent: profile, project state, decisions
│ ├── user-profile.md
│ ├── project-state.md
│ └── decisions-log.md
├── Agent-<Name>/ # per-agent working context + daily logs
│ ├── working-context.md
│ ├── mistakes.md
│ └── daily/YYYY-MM-DD.md
└── ...
Install the Obsidian desktop app and open the cloned folder as a vault. That's the whole setup — the app is a viewer; the files are the truth.
The symlink
My agent runtime wants to write daily memory files into its own workspace. I want those files in the vault, where I can actually read them and where they get backed up with everything else. One symlink resolves the disagreement:
ln -s ~/obsidian/<vault>/Agent-<Name>/daily ~/.openclaw/workspace/memory
The agent writes to a path it believes it owns; the bytes land in the vault. Recreate this on the new machine — git does preserve symlinks, but only if the target path exists, so verify it resolves:
ls -la ~/.openclaw/workspace/memory
Rebuild the memory index
Semantic search over the vault runs off a local index that is not in git. Rebuild it:
openclaw memory index --agent <agent-id> --force
memory status --fix reports success and does nothing when the index database is
missing entirely. Only index --force actually builds it. If memory search returns
zero results on a fresh machine and status claims everything is fine, this is why.
09launchd: making it survive reboots
Every long-running piece gets a launchd agent in ~/Library/LaunchAgents. Mine runs
eleven: the primary gateway, one per persona profile, plus backup, healthcheck, and network watchdog
jobs.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key> <string>ai.agent.gateway</string>
<key>RunAtLoad</key> <true/>
<key>KeepAlive</key> <true/>
<key>ProcessType</key> <string>Interactive</string>
<key>ThrottleInterval</key> <integer>30</integer>
<key>ExitTimeOut</key> <integer>25</integer>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/node/bin/node</string>
<string>/opt/homebrew/lib/node_modules/openclaw/dist/index.js</string>
<string>gateway</string>
</array>
<key>WorkingDirectory</key> <string>/Users/<you>/.openclaw</string>
<key>StandardOutPath</key> <string>/Users/<you>/Library/Logs/agent/gateway.log</string>
</dict>
</plist>
Three keys in there are doing more than they look like they are:
-
ProcessType: Interactive— the default (Background) gets aggressively CPU- and I/O-throttled by macOS. An agent that must answer a chat message in under a second cannot be a background task. This one line is the difference between "responsive" and "why did that take ninety seconds." -
ThrottleInterval: 30— raises launchd's 10-second minimum respawn delay. If the process is crash-looping on bad config, the default hammers launchd into a respawn storm and buries the real error in log noise. -
ExitTimeOut: 25— graceful drain headroom before launchd escalates from SIGTERM to SIGKILL. SIGKILL mid-write on a SQLite state file is how you get a corrupt database.
The environment wrapper
launchd gives a process almost no environment — not your PATH, not your shell exports. Rather than stuffing values into the plist (where they end up in git), point the plist at a wrapper that sources an env file:
#!/bin/sh
set -eu
env_file="$1"; shift
[ -f "$env_file" ] && . "$env_file"
exec "$@"
Then ProgramArguments becomes [wrapper.sh, env-file, node, …]. The env
file stays out of version control; the plist is safe to commit.
Loading
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.agent.gateway.plist
launchctl print gui/$(id -u)/ai.agent.gateway | head -20 # state, pid, last exit
# to restart after a config change
launchctl kickstart -k gui/$(id -u)/ai.agent.gateway
10Backups: two systems, on purpose
Config is text and belongs in git. State is multi-gigabyte SQLite and does not. So there are two jobs, and neither one alone is a complete backup.
System 1 — nightly git auto-commit
A loop over the brain repos. Add, skip if nothing staged, commit with a UTC timestamp, push.
#!/usr/bin/env bash
set -uo pipefail
PATH="/opt/homebrew/bin:/usr/bin:/bin"
LOG="$HOME/.local/log/backup-brains.log"
REPOS=(
"$HOME/.openclaw/workspace"
"$HOME/.hermes"
"$HOME/.hermes/profiles/a"
"$HOME/.hermes/profiles/b"
"$HOME/obsidian/<vault>"
)
for repo in "${REPOS[@]}"; do
[ -d "$repo/.git" ] || continue
cd "$repo" || continue
git add -A
git diff --cached --quiet && continue # nothing changed, skip
git commit -q -m "auto $(date -u +%Y-%m-%dT%H:%M:%SZ)"
git push -q origin main >> "$LOG" 2>&1
done
The git diff --cached --quiet && continue line matters more than it looks — without it
you get an empty commit every night and the history becomes useless for actually finding when
something changed.
System 2 — encrypted disaster-recovery archive
This one covers everything git can't: the state databases, the machine config, the secrets, the plists. It runs nightly, encrypts locally with a passphrase from the keychain, and only then uploads off-host.
The shape of it:
- Acquire a lock directory so two runs can't snapshot the same database simultaneously.
- Copy config files:
config.yaml,auth.json, cron jobs, skills, machine config, all plists. -
Snapshot every SQLite database with the online backup API —
sqlite3 src ".timeout 60000" ".backup dest". A plaincpof a database a live gateway is writing to gives you a corrupt file, and you don't find out until you need it. - Write a manifest with a SHA-256 for every file.
- Tar, then
gpg --symmetric --cipher-algo AES256with the keychain passphrase. - Verify by decrypting and listing the archive before uploading. An unverified backup is a guess.
- Upload to cloud storage, then prune archives older than the retention window.
# reading the passphrase without ever putting it in argv or history
passphrase=$(security find-generic-password -a "$USER" -s "agent-dr-backup-key" -w)
printf '%s' "$passphrase" | gpg --batch --yes --pinentry-mode loopback \
--passphrase-fd 0 --symmetric --cipher-algo AES256 \
--output "$ARCHIVE.gpg" "$ARCHIVE"
unset passphrase
The profile loop iterates profiles/* rather than a hardcoded list. Every time I've
written a static list of things to back up, I've later added a thing and not updated the list —
and discovered the omission during a restore, which is the worst possible time.
11Scheduled jobs
Two categories, and they belong in different places.
System jobs — backups, token refresh, health checks, watchdogs — are launchd agents
with a StartCalendarInterval. They're plain shell and shouldn't depend on an agent
being awake.
Agent jobs — morning briefings, ops rollups, report generation — go in the agent runtime's own scheduler, because they need model access and tools.
openclaw cron list
openclaw cron get <job-id>
Durability. Depending on the runtime, a job created through the default path can be in-memory only and silently vanish on gateway restart. Check for a durable/persistent flag and set it, then verify the job survives a restart before you trust it.
Tool allow-lists. On my setup, a non-empty tool allow-list on a scheduled job gets
rewritten at save time and drops every external MCP tool. The job doesn't error — it runs with
native tools only, the model improvises, and the output reads fine while entire data
sources are missing. That is a genuinely nasty failure mode. If a job needs MCP data, leave the
allow-list empty and confirm with cron get after every single edit, because subsequent
edits can re-inject it.
One more: a cron expression can't express two different times of day. "7am weekdays, 8am Saturday" is two jobs, not one. I lost a Saturday briefing to that assumption.
12Verification checklist
Don't declare victory on "the process is running." Walk this list.
launchctl print gui/$(id -u)/<label>shows a stable pid — check it twice, a minute apart, so you catch a crash loop- Health check command reports all agents healthy
- Send a message on every chat channel and get a real reply on each
mcporter testpasses for every MCP server, not just the first one- Memory search returns real results (not zero — see the index trap in section 8)
- The vault symlink resolves and a test write lands in the right place
- Force-run each backup script manually and confirm a fresh artifact appears at the destination
- Decrypt yesterday's archive and list its contents. This is the only step that proves the backup is real
- Every scheduled job appears in the list with a sane next-run time
- Reboot the Mac. Everything comes back with no keyboard involvement
That last one is the actual test. A stack that needs you to type something after a power cut isn't automated, it's just a program you happen to be running.
13Gotchas that cost me a night each
Everything below is something I learned the expensive way. In rough order of how much time it ate.
Config changes don't take effect until a later restart
If behavior contradicts what the config plainly says, compare the process start time against the config file's mtime before you debug the setting. I have lost hours to a config that was correct on disk and irrelevant in memory. It's the first thing to check, not the last.
Rejected config reloads look like silence, not errors
A malformed config can cause a running gateway to refuse the reload and keep serving the old one. No crash, no visible error — the change just doesn't happen. When an edit "does nothing," check the logs for a reload rejection before editing anything else.
Empty string can mean "all"
In at least one config I use, an empty channel allow-list means every channel, not none. And environment variables can override the YAML entirely, so the file you're reading may not be the config that's in force. Read the actual precedence rules for any security-relevant setting rather than assuming the obvious interpretation.
An agent can lock itself out
If an agent has write access to its own config, it can blank its own allowlist and then be unable to fix it — because fixing it requires the access it just removed. The process is alive and logging "unauthorized user" on every message. Keep an out-of-band way to restart and edit config that does not route through the agent.
Global npm tooling breaks on new Node majors
My deploy CLI silently fails with an unhelpful fetch failed on the current Node major.
npx -y <tool>@latest pulls a build that works. If a global tool starts failing right
after a Node upgrade, try npx before you debug the tool.
Verify generated artifacts by content, not by metadata
I had a PDF fit-check loop reading page count from Spotlight metadata. On a file written seconds ago, Spotlight hasn't indexed it yet and returns null — so the check passed on everything, including a two-page document that was supposed to be one page. Count the actual bytes:
python3 -c "d=open('out.pdf','rb').read(); print(d.count(b'/Type /Page')-d.count(b'/Type /Pages'))"
Diff automated edits before they commit
A blind sed in a deploy script corrupted a config pin and took a production service down
for four minutes. Grepping for the new value afterward did not catch it, because the new
value was present — it was just present in a mangled line. Print the diff and validate the file
parses before committing anything a script generated.
Don't let two writers touch one file
If you delegate work to a background process that writes files, don't edit those files while it runs. There's no lock. My careful mid-run fix was silently reverted when the background job wrote its version. Re-read any file a delegated process touched.
The 100 MB wall arrives without warning
A state database grows quietly until a push fails, and by then you may have committed it and need to
rewrite history to get it out. Decide up front which files are git-backed and which are
archive-backed, and put the archive-backed ones in .gitignore on day one.
14Driving all of this with an agent
There's a nice irony in hand-typing a two-hour rebuild of a machine whose entire purpose is running agents. You don't have to. Below are the prompts I'd actually paste, in order, into a coding agent with shell access on the new Mac — Claude Code, Codex, Cursor, whatever you run.
They're written to be pasted verbatim. Each one names the section it implements, states what "done" means, and tells the agent what it is not allowed to do. That last part matters more than the instructions themselves: an unbounded agent with shell access on a fresh machine will cheerfully invent a plausible path, write a config that doesn't match your install, and report success.
One prompt per turn, in order, and read the output before pasting the next one. Do not paste all eight at once. Each stage depends on facts discovered in the one before it, and a wrong assumption in stage 2 becomes a broken launchd job in stage 6 that looks like a completely unrelated bug.
Ground rules — paste this once, first
This is the frame everything else runs inside. Most of it is scar tissue from section 13 turned into standing instructions.
Prompt 0 · ground rulesYou're helping me rebuild my AI agent stack on a fresh Mac. We'll work in
stages; I'll paste one stage at a time. Follow these rules for the whole
session.
FACTS
- Never invent a path, port, filename, plist label, or config key. If you
need one and don't have it, read it off the machine or ask me.
- Before editing any file, read it. Before creating one, check whether it
already exists.
- If something contradicts what I told you, say so instead of picking one.
VERIFICATION
- Verify by content, never by exit code or HTTP status alone. Grep for a
string you expect to find.
- A process being alive is not a passing test. Check the pid twice, sixty
seconds apart, so a crash loop can't look like success.
- Never report a step complete without pasting the command output that
proves it. "Should now be working" is a failure.
- If a check fails, say so plainly and stop. Do not work around it silently.
SAFETY
- Ask before anything destructive or hard to reverse: rm -rf, force push,
overwriting an existing config, revoking a credential, killing a process
you didn't start.
- Never write a secret into a file that git tracks. Never print a full
token, key, or password into the transcript — mask to the last 4 chars.
- Don't install anything globally that a project-local install can cover.
SCOPE
- Do exactly the stage I gave you. Don't start the next one.
- Don't refactor, rename, or tidy anything you weren't asked to touch.
- At the end of each stage, output: what you did, what you verified and
how, and anything you had to guess. The guesses list is the important
one — if it's empty, say so explicitly.
Stage 1 — inventory the machine you're leaving
Run this one on the old Mac, while it still works. It's the highest-value prompt on this page. This entire runbook is generic; what it can't know is your labels, your ports, your profile names. The output of this prompt is the machine-specific other half — and it's read-only, so it's safe to run before you've decided anything.
Prompt 1 · inventory (old machine, read-only)Read-only task. Do not modify, move, or delete anything.
Produce a migration inventory for this Mac's agent stack and write it to
~/Desktop/stack-inventory.md. Include:
1. Runtimes: every agent/gateway process running, its version, its install
path, and how it was installed (brew, npm -g, git clone).
2. launchd: every user LaunchAgent I own — full label, plist path, the
program arguments, working directory, env vars, and whether it's
currently loaded. Copy the plists into ~/Desktop/stack-inventory/plists/.
3. Ports: every port these processes listen on, and which process owns it.
4. Repos: for each git repo under my home dir that's part of this stack —
local path, remote URL WITH ANY CREDENTIAL REDACTED, current branch,
and whether the working tree is dirty.
5. Secrets inventory: list the NAMES and locations of every credential file
and env var the stack reads. Never print a value. Flag anything stored
in plaintext, and specifically flag any git remote URL with a token
baked into it.
6. MCP servers or tool integrations that are configured, with their
transport (stdio/http) and auth type — names only, no tokens.
7. Scheduled jobs: crontab, any scheduler the agent runtime owns, and any
launchd job with a StartCalendarInterval. Include schedules.
8. Data sizes: the size of every state/database file, flagging anything
over 100 MB, and whether each is git-tracked or gitignored.
For each item say how you determined it — the command you ran. If you
couldn't determine something, list it under "UNKNOWN" rather than guessing.
Then append a section: "What must be re-authenticated by hand on the new
machine" — every OAuth login, keychain item, or 2FA-gated service that
cannot be copied across.
The UNKNOWN list and the re-auth list are the parts worth your attention. Everything else the agent can rediscover; those two are what strand you at 11pm on a machine that no longer boots.
Stage 2 — base system and secrets
Sections 02 and 03. Secrets go in before any runtime starts, not after.
Prompt 2 · base system + secretsStage: base system. Implements sections 02-03 of the runbook.
1. Install Homebrew if absent. Confirm it landed in /opt/homebrew (Apple
Silicon) and that brew shellenv is in ~/.zprofile.
2. brew install node git gh ripgrep poppler pnpm
3. Print the version of each and the node major. If the node major is
newer than the runtime I'm about to install supports, tell me now
rather than after something fails weirdly.
4. Create the secrets directory with 700 perms. For each credential in
my inventory file, create a placeholder entry and tell me what to
paste — I will paste the values myself. Do not ask me to send you a
secret in chat.
5. Verify the secrets dir and every file in it are not world-readable,
and that the path is covered by .gitignore in any repo that could
reach it.
Do not install any agent runtime yet. Stop when the base is verified and
give me the list of secrets still needing values.
Stage 3 — brain repos and the vault
Sections 04 and 08. This is the stage where the machine stops being empty.
Prompt 3 · repos + vaultStage: restore the brain. Implements sections 04 and 08.
1. Set up SSH auth for GitHub: generate a key if none exists, add it to
the agent and the keychain, print the public key for me to paste into
GitHub, then verify with `ssh -T git@github.com`.
2. Clone these repos to these exact paths: [paste from your inventory]
Use SSH remotes. If any clone command you write contains a token,
stop and tell me — that's the bug from section 02.
3. Restore the Obsidian vault to its path from the inventory and verify
any symlink into it resolves to a real directory, by writing a test
file through the symlink and reading it back from the true path.
Delete the test file afterward.
4. Confirm every restored repo's gitignore still excludes state
databases and secret files BEFORE anything writes to them. Print the
relevant ignore lines.
5. Report the size of the vault and the number of markdown files, so I
can sanity-check it against the old machine.
Do not start any runtime. Do not commit anything.
Stage 4 — runtimes
Prompt 4 · install the runtimesStage: runtimes. Implements sections 05-06.
Install the primary agent runtime, then the secondary runtime and its
named profiles, using the versions and install methods from my inventory.
Rules specific to this stage:
- Start each one in the FOREGROUND first and show me the startup log.
Nothing goes under launchd until it has started cleanly by hand.
- If a config edit appears to have no effect, compare the process start
time to the config file mtime before debugging the setting itself.
Config changes are not live until a later restart.
- After any config reload, grep the log for a rejected/invalid config
message. A rejected reload is silent — the old config just keeps
serving and the change never happens.
- Check the auth/allowlist settings carefully. In at least one config an
empty allow-list means ALL, not none, and env vars can override the
YAML file entirely. Tell me the effective value, not the file value.
Verify by sending a real message through each runtime and showing me the
reply. Then stop.
Stage 5 — MCP servers
Section 07. Expect this stage to be the slowest, because half of it is you clicking through consent screens rather than the agent doing anything.
Prompt 5 · MCP serversStage: MCP servers. Implements section 07.
Reconnect each MCP server from my inventory, one at a time. For each:
1. Add the config entry (correct transport — stdio vs http).
2. Test it in isolation before adding the next one.
3. Show me the tool list it returns. An empty tool list is a failure,
not a pass.
For anything OAuth-based, generate the consent URL and hand it to me —
I'll complete the login. Then verify the token landed where the runtime
actually reads it from, which is not always where the CLI wrote it.
If a stdio server hangs, check the framing convention before assuming
the server is broken.
At the end, list every server as PASS with its tool count, or FAIL with
the error. Do not summarize a partial failure as "mostly working."
Stage 6 — supervision, backups, schedules
Sections 09 through 11 — the ones that decide whether this survives a power cut.
Prompt 6 · launchd + backups + cronStage: supervision and backups. Implements sections 09-11.
1. Write the launchd plists using the labels, paths, and env from my
inventory. Load them and verify each shows a stable pid — check
twice, sixty seconds apart, to catch a crash loop.
2. Restore the backup scripts. Two systems: git for text/config,
encrypted archive for state databases. Confirm the state DBs are
gitignored and the archive destination is reachable.
3. Force-run every backup script manually. Show me a fresh artifact at
the destination with today's timestamp.
4. THEN decrypt the archive you just created and list its contents. A
backup that has never been restored is not a backup. This step is
not optional and cannot be skipped for time.
5. Recreate the scheduled jobs with their schedules from the inventory.
Print each one's next run time and confirm the timezone is what I
expect.
If any script contains an automated in-place edit (sed, awk, a rewrite
step), print the diff it produces on a dry run before you let it near a
real file.
Stage 7 — make it prove itself
Section 12, as an acceptance test rather than a checklist. The framing here is deliberate: the agent is told up front that a clean pass is suspicious, which is a surprisingly effective way to stop it rubber-stamping its own work.
Prompt 7 · acceptance testStage: verification. Implements section 12.
Run the full checklist and give me a table: check | PASS/FAIL | the
command you ran | the output that proves it.
- launchd shows a stable pid for every job (checked twice, 60s apart)
- health check reports all agents healthy
- a real message on EVERY channel gets a real reply
- every MCP server tests clean, with a non-empty tool list
- memory/index search returns actual results, not zero
- the vault symlink resolves and a test write lands in the right place
- each backup script produces a fresh artifact
- yesterday's encrypted archive decrypts and lists
- every scheduled job has a sane next-run time in the right timezone
Assume something is broken. If everything passes on the first attempt,
say which checks you consider weakest and re-test those specifically —
a clean sweep usually means a check is measuring the wrong thing.
Then tell me exactly what I need to do by hand, and stop. Do not reboot
the machine yourself.
The reboot is mine to run, and it's the real test: power cycle the Mac and confirm everything comes back with no keyboard involvement.
What I don't hand to an agent
Worth being explicit, because the failure modes here are expensive and none of them announce themselves.
- Entering secrets. I paste values into files myself. An agent that has read a token has put it in a transcript, and transcripts get logged, synced, and summarized.
- Revoking anything. Rotation order is load-bearing — rewrite the remotes first, then revoke. An agent that helpfully revokes first breaks every automation at once and you find out which ones the hard way.
- Giving an agent write access to its own config. It can blank its own allowlist and then lack the access required to fix it. Section 13 covers what that looks like from the outside: alive, logging, and completely unreachable.
- The reboot test. If the thing verifying survival is running on the machine being rebooted, it isn't verifying anything.
Make the agent show its evidence at every stage. Almost every bad outcome I've had with a coding agent traces back to accepting "done" without asking what proved it — and on a fresh machine, where nothing is familiar enough for a wrong answer to look wrong, that habit is the whole difference between a two-hour rebuild and a two-day one.
15What this actually buys
Roughly two hours, mostly waiting on installs and OAuth consent screens. At the end you have a machine where agents come back after a power cut, memory survives a disk failure, and secrets live in a keychain instead of a dotfile.
The part I'd emphasize if you take nothing else: the vault is the important layer. Runtimes get replaced — I've swapped model providers, changed frameworks, rewritten the whole supervision setup. The markdown files carry forward through all of it. Config in git, state in an encrypted archive, and the thing the agent actually knows in plain text you can read yourself.
Build the backup before you build the second agent. The restore is only as good as the last time you tested it — which, for most people, is never.