Linear runbook for a fresh macOS install. Walk from the top.
- Pre-install gates (steps 1–4): things
install.shcan't do. - Run
./install.sh(step 5). - Post-install (steps 6–12): identity, permissions, projects.
Required for the mas block of the Brewfile. mas cannot sign in or prompt — it uses the App Store GUI's session.
- Open the App Store, click your account icon (bottom-left), sign in.
- Install one free app manually (e.g. Apple Configurator 2). If this fails,
maswill fail the same way. - Leave App Store running during
install.sh.
If the manual install errors with ISErrorDomain Code=-128 "Unknown Error": check your phone for a "new device" approval, allow up to 24h for Apple ID security review, or confirm a payment method is on file.
Important
Virtualized macOS — skip this step. Per Apple Support 120468, the App Store is not available in VMs. install.sh detects VMs (sysctl -n hw.model matching VirtualMac*/VMware*/Parallels*/QEMU) and skips the mas block automatically.
install.sh runs xcode-select --install automatically. Click Install in the popup and wait for it to finish before proceeding past Phase 1.
Tip
On a VM, the CLT dialog can hide behind a full-screen terminal. Shrink the terminal if you don't see it.
swiftlint requires full Xcode (~15 GB, App Store). Skip unless you need it; the rest of install.sh runs fine without.
System Settings → Privacy & Security → toggle your terminal on both:
- App Management — required for
.pkgcasks (docker-desktop, microsoft-office, etc.) to write to/Applications. - Full Disk Access — required for
scripts/macos-defaults.shto write Safari preferences (sandboxed app).
If macOS prompts to restart the terminal, accept. Granting these mid-install means re-running the script from scratch, so do it now.
cd ~/dotfiles
./install.shExpect multiple password prompts during .pkg cask installs (these bypass sudo cache by design). brew bundle partial failures are tolerated — the script continues to Phases 2-5. Re-run after fixing failures; every phase is idempotent.
-
Launch the 1Password app, sign in via Emergency Kit (account URL, email, Secret Key, Master Password).
-
Settings → Developer → enable Use the SSH agent.
-
Settings → Security → enable Touch ID unlock.
-
Verify:
op whoami ssh-add -L
gh auth login # GitHub.com → HTTPS → browser
gh auth statusLaunch Docker Desktop from /Applications, accept the license, sign in if required. Verify with docker ps.
System Settings → Privacy & Security:
- Full Disk Access: enable for VS Code and any backup/sync tools (Terminal was already done in step 4).
- Accessibility: grant to window-management / automation tools you've installed.
- Developer Tools: enable for your terminal.
Touch ID & Password: enroll Touch ID. (install.sh already configured PAM to use it for sudo once enrolled.)
Browser: install the 1Password browser extension on first launch.
If your services are behind a VPN (UniFi Teleport / WireGuard / Tailscale / etc.), connect now. Verify:
ping <some-internal-host>For each project, follow its own docs/operator-bootstrap.md (or equivalent). Project bootstrap typically pulls SOPS age keys, kubeconfig, and Ansible vault passwords from 1Password via op read.
~/dotfiles/bin/verify-workstation.shNote
Script not built yet — backlog item. Manual sanity check: new terminal shows Smyck + glyphs, op whoami, gh auth status, kubectl get nodes (if a project's been bootstrapped).
Found a gap on a fresh-machine bootstrap? Fix it here in the same session and commit with a docs(FIRST-RUN): prefix. If the gap could be automated, also bump install.sh.