Operator runbook

Rebuilding an AI agent stack on a new Mac

A handful of always-on AI agents that answer messages, pull ops data, file reports, and remember things across months — all of it running on a single Mac. This is the order of operations for standing the whole thing back up on fresh hardware, plus every gotcha that has cost a night, so it doesn't cost you one.

If you'd rather not type it, section 14 is the same rebuild as eight copy-pasteable prompts for a coding agent with shell access — including the read-only one you should run on the old machine first.

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.

LayerWhat it isWhere 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
Version pinning

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
Use SSH, not tokens in URLs

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
Rewriting the remote is not rotation

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
Rotate on migration

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
Keep these private

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:

KeyWhat it controls
agentsAgent list, per-agent workspace, persona, model, fallbacks
channelsChat platform connections — bot tokens, allowlists, per-channel behavior
mcpExternal tool servers (section 7)
modelsProvider routing and fallback order
toolsWhat the agent is allowed to call; alsoAllow extends, allow restricts
memoryMemory index configuration
Model fallbacks are not optional

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/
Each profile needs its own auth

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
Test before you restart

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
The --fix flag is a trap

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:

  1. Acquire a lock directory so two runs can't snapshot the same database simultaneously.
  2. Copy config files: config.yaml, auth.json, cron jobs, skills, machine config, all plists.
  3. Snapshot every SQLite database with the online backup API — sqlite3 src ".timeout 60000" ".backup dest". A plain cp of a database a live gateway is writing to gives you a corrupt file, and you don't find out until you need it.
  4. Write a manifest with a SHA-256 for every file.
  5. Tar, then gpg --symmetric --cipher-algo AES256 with the keychain passphrase.
  6. Verify by decrypting and listing the archive before uploading. An unverified backup is a guess.
  7. 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
Discover, don't enumerate

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>
Two scheduling traps

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 test passes 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.

How to run these

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 rules
You'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.
Read that file before you wipe anything

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 + secrets
Stage: 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 + vault
Stage: 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 runtimes
Stage: 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 servers
Stage: 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 + cron
Stage: 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 test
Stage: 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.
The one habit that matters

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.