Runtime state is not configuration: what a Nix bootstrap cannot restore
OpenCode plugins, agent session history, and the Zen browser profile live outside Nix, plus the small fixes from the first day of using the macOS setup.
Part 9 ended by promising an implementation of the three-tier OpenCode setup. I have not built it. The day after Parts 6 to 9, I fixed a handful of small bugs and found a category of state that no bootstrap script restores: OpenCode plugins, agent session history, and the Zen browser profile.
The Lima VM runs Ubuntu
Part 8 first described the OpenCode isolation VM as a custom NixOS image built with nixos-generators. I replaced it within hours, and Part 8 now describes the current setup. The VM runs Ubuntu 24.04 LTS, and the provision script installs the multi-user Nix daemon:
# lima/opencode.yaml, provision section
provision:
- mode: system
script: |
#!/bin/bash
set -euo pipefail
if ! command -v nix &>/dev/null; then
curl -L https://nixos.org/nix/install | sh -s -- --daemon --yes
fi
The NixOS image needed an aarch64-linux builder and a transfer to macOS for every update. With Ubuntu, Lima downloads the official cloud image, the script installs Nix, and the VM uses the same Nix config as the host. The nixosConfigurations.opencode-lima output in flake.nix stays as a reference design.
Small fixes from the first day
Aerospace. Alt+Return collided with a terminal shortcut, so I renamed it to Alt+Enter and fixed the comment that described it.
Sketchybar. The bar subscribed all 9 workspace items to the aerospace_workspace_change event, so each switch ran the highlight script nine times. A single ws_monitor item now owns the event handler. A startup query (aerospace list-workspaces --focused) highlights the current workspace before the first event fires.
Python tools. code-review-graph in ai-tools.nix was pinned to a uv-managed CPython 3.14 to get C headers for the watchdog extension. The build failure had a different cause: the SDKROOT and CPATH variables set earlier in the same module for Xcode SDK headers. Once I saw that, I dropped the --python 3.14 pin.
Fonts. nix-darwin's fonts.packages does not reliably register fonts with Core Text on every macOS version. I moved the Nerd Fonts (FiraCode, JetBrains Mono, Hack) to Homebrew casks, which install into ~/Library/Fonts/.
Locale. LANG and LC_ALL were set to en_US.UTF-8 in .env and .profile, but fish and some tools expect en_US.utf8. Locale-dependent tools fell back to the C locale without any error.
Homebrew PATH. The fish config called brew shellenv to put Homebrew on the PATH, but brew is not on the PATH until that command runs. It now calls /opt/homebrew/bin/brew shellenv.
Bootstrap scripts. mac-setup.sh got an AnythingLLM.app bundle-name fix and switched from fish -c to fish -ic for its abbreviation and starship checks, because abbreviations load only in interactive shells. mac-restore.sh changed from a Google Drive and rclone flow to a generic restic repository prompt that accepts a local drive, NAS, rclone remote, or SFTP path. That cut it from about 787 lines to 359.
The migration gap
The bootstrap scripts restore everything Nix declares: packages, config files, services, environment variables. Three things live outside that model.
OpenCode config and skills
OpenCode's directory mixes tracked and untracked files:
~/.opencode/
├── opencode.json → ~/dotfiles/.opencode/opencode.json (symlinked, tracked)
├── bin/ binary, reinstalled by mac-setup.sh
├── node_modules/ generated by npm install
├── plugins/ installed at runtime
└── config.json created by OpenCode itself
creatlinks.sh re-links opencode.json on every home-manager switch, so that file is covered. Nothing backs up or restores the rest of ~/.opencode/. On a new machine you have to:
- Install the OpenCode binary with
curl -fsSL https://opencode.ai/install | bash. - Run
creatlinks.shto recreate the config symlink. - Reinstall skills with
npx skills installfor lockfile-tracked skills, andnpx skills add <repo>for anything added ad hoc. - Let OpenCode update its own binary and plugins.
Steps 1 to 3 are manual. The binaries and plugin packages are disposable, but you need to know to reinstall them. Part 13 later moved the skills themselves into the flake.
Agent session history
~/.agent-work/ holds research/, plans/, specs/, and status/, and its .gitignore contains *, so nothing in it is tracked. I made it ephemeral on purpose, because agent outputs are intermediate artifacts. On migration, session history is gone unless you copy it by hand. creatlinks.sh recreates the directories empty.
If you build something similar, decide up front whether agent outputs are scratch space (not backed up or restored, which is my choice) or a knowledge store (committed, indexed, searchable, which needs a different design). I chose scratch space because the outputs are cheap to regenerate. Later I added a nightly job that backs up sanitized sessions, described in Part 15.
Zen Browser profile
Nix manages the Zen binary and its extensions (uBlock Origin, ClearURLs, and others) in browser.nix. The profile lives in ~/.zen/, and no script touches it. A fresh bootstrap gives you Zen with the extensions and an empty profile. Firefox Sync recovers bookmarks, but session history and extension settings are lost. The native-messaging bridge that lets Walker search browser bookmarks is Nix-managed and redeploys automatically.
What the three have in common
All three are runtime state. Nix describes configuration, and it does nothing for files that programs write while they run. ~/.opencode/ is not a config file, ~/.agent-work/ is not a package, and ~/.zen/ is not a dotfile. Once I sorted files by that boundary, it was clear which tool should handle each one.
Roadmap status
The three-tier agent design from Part 9 has a design document, a threat model, and drafted agent prompts. It does not have:
- the web-fetch sanitisation MCP proxy (domain whitelist, injection scanning)
- the git wrapper MCP (branch protection, no push to main)
- the ticket tracker MCP scoping (read-only for
@research) - the shared workspace handoff protocol
- role-based agent definitions in the OpenCode config
The design is at ~/dotfiles/docs/handoff/opencode-multi-agent-setup.md. I have moved my target from a calendar quarter to "after the daily setup runs for a month without needing fixes". Here is the milestone table from Part 5 with current status:
| Milestone | Original target | Current status |
|---|---|---|
| gVisor sandbox + MCP broker | Q3 2026 | Not started |
| Bedrock token minter | Q4 2026 | Partially done (scripts exist, token injection into sandbox does not) |
| Systemd supervisor | Q4 2026 | Not started |
| Firecracker microVMs | Q4 2026 | Not started |
The token minter (create-bedrock-role.sh plus the bedrock-token launchd agent from Part 9) works. The missing piece is injecting the token into a sandbox that does not exist yet.