Paste everything below the line into a fresh claude session on the new machine.
Written for Apple Silicon macOS (Tahoe 26.x / Sequoia 15.x), Claude Code, and a
human who lives in the terminal.
Why this exists: most "new Mac setup" gists are a wall of brew install. This is a
prompt β it gives an agent the constraints, the decision points, the ordering, and
the verification step, so you get a working machine instead of a half-applied script.
| File | What it is |
|---|---|
macbook-pro-2026-setup-prompt.md |
this file β the prompt you paste into Claude Code |
bootstrap.sh |
idempotent executable version of PHASES 1β9. Run ./bootstrap.sh --dry-run first. |
zsh_plugins.txt |
antidote bundle β ~/.zsh_plugins.txt (Oh My Zsh plugins, no Oh My Zsh) |
starship.toml |
lean two-line prompt β ~/.config/starship.toml |
p10k.zsh |
optional alternative prompt β Powerlevel10k config β~/.p10k.zsh |
claude-wrapper.zsh |
claude() shell wrapper β switch between API key / OAuth token, permissive by default |
Two ways to use this:
- Agent-driven (recommended) β paste this file into
claude. It asks the DECISIONS, adapts, and explains. - Script-driven β
./bootstrap.sh --dry-runto preview, then run it. No sudo, resumable via--from <phase>.
They compose: run bootstrap.sh for the mechanical 80%, then hand the agent this file for the judgement calls.
You are setting up a brand-new MacBook Pro (Apple Silicon) for a senior engineer who is a terminal power user and works with AI agents daily. You have shell access via your Bash tool. Treat this machine as a long-lived primary workstation.
Your job is not to run a script. Your job is to build a reproducible, agent-friendly development environment and leave behind the artifacts that let it be rebuilt in 20 minutes.
- Idempotent. Every step must be safe to re-run. Guard with
command -v x >/dev/null ||,brew list x || brew install x,grep -q ... file || echo ... >> file. Never blindly append. - Never clobber. Before overwriting any dotfile,
cpit to~/.setup-backup/<timestamp>/. - Checkpoint. Maintain
~/.setup-state.jsonβ{phase, completed: [], skipped: [], notes: []}. Update it after every phase. If the session dies, a fresh agent must be able to read it and resume. - Ask before : anything needing
sudo, changing the login shell, installing a kernel extension or VPN, touching System Settings that affects security, or spending money (paid apps, cloud resources). - Batch the questions. Ask all open decisions in ONE round up front (see DECISIONS), not one at a time across an hour.
- Prefer
brewformulae/casks over curl-pipe-bash. When upstream only ships an installer script, read it first and say what it does. - Show, don't claim. End each phase with the actual command output proving it worked. "Installed successfully" without output is not acceptable.
- No secrets in files. No API keys in
.zshrc, ever. See PHASE 8.
| # | Question | Default if user says "you pick" |
|---|---|---|
| 1 | Shell: zsh (macOS default, universal) orfish (best interactive UX, non-POSIX)? | zsh β agents and scripts assume POSIX |
| 1b | Plugin manager: antidote (static file + deferred ; loads Oh My Zsh plugins without Oh My Zsh),Oh My Zsh (simpler, slower), or plain? | antidote |
| 1c | Prompt: starship (cross-shell, TOML) orPowerlevel10k (zsh-only, instant prompt)? | starship |
| 2 | Terminal: Ghostty (fast, native, 2026 default),WezTerm (Lua-scriptable),iTerm2 (mature),Warp (AI-native, telemetry)? | Ghostty |
| 3 | Multiplexer: tmux ,Zellij , or terminal-native splits only? | tmux (agents/ssh assume it) |
| 4 | Dotfile strategy: chezmoi ,GNU stow , orbare git repo ? | chezmoi (templating + secrets integration) |
| 5 | Containers: OrbStack (fast, low battery),Docker Desktop ,colima , or none? | OrbStack |
| 6 | Password/secret manager already in use? (1Password, Bitwarden, keychain-only) | ask β do not guess |
| 6b | Editor/IDE: VS Code (or a fork: Cursor/Windsurf/Kiro),JetBrains ,Zed ,Neovim , or terminal-only? | ask β this is the most personal choice here |
| 7 | Which languages actually matter? (node/py/go/rust/ruby/java/β¦) | node + python + go, add on demand |
| 8 | Personal or work machine? (changes SSH/GPG signing, MDM constraints, telemetry tolerance) | ask |
Also run recon before asking, so your questions are informed:
sw_vers; uname -m; echo "---"
command -v brew git zsh fish ghostty antidote starship fnm uv go claude 2>/dev/null
echo "---"; ls -a ~ | head -50
echo "---"; csrutil status 2>/dev/null; fdesetup status 2>/dev/null
echo "---"; profiles status -type enrollment 2>/dev/null # is this MDM-managed?
-
Xcode Command Line Tools:
xcode-select -p || xcode-select --install(this is interactive/GUI β tell the user to click through, then poll untilxcode-select -psucceeds). -
Homebrew at
/opt/homebrew(arm64). Add the shellenv line to~/.zprofile, not~/.zshrc. -
Create a
Brewfilefrom the start in the dotfiles repo. Every install goes through it:brew bundle --file=~/dotfiles/Brewfile. This is the reproducibility artifact. -
mkdir -p ~/.local/bin ~/Developer ~/.configand ensure~/.local/binis first onPATH. -
Rosetta only if the user names an actual x86-only dependency. Do not install it "just in case".
-
Terminal emulator per DECISION 2. Config in
~/.config/ghostty/config(XDG, git-trackable). -
Nerd Font β required or every glyph in the prompt renders as tofu:
brew install --cask font-jetbrains-mono-nerd-font(or Maple Mono NF / Berkeley Mono if owned). Set it in the terminal config, and verify by echoing a glyph. -
Prompt: starship β
brew install starship, config at~/.config/starship.toml. See PHASE 2b. Keep it fast β setcommand_timeoutand disable modules that stat the filesystem on every prompt in big repos. A slow prompt is a tax you pay thousands of times a day. -
Multiplexer per DECISION 3, config in
~/.config/. -
Plugin manager + prompt: antidote + starship β see PHASE 2b, big enough for its own phase.
-
Benchmark startup and report the number :
hyperfine --warmup 3 'zsh -i -c exit'. Target < 150ms. If it's over, bisect withzsh -xvand fix it now, not later.
antidote is the plugin manager; starship is the prompt. Neither is Oh My Zsh β but you keep OMZ's plugin library, which is the part actually worth having.
Oh My Zsh is two things bolted together: a plugin library (excellent, ~300 plugins, ~150 git aliases) and a **framework ** (sources everything serially on every shell start, 150β400ms). antidote lets you keep the first and throw away the second:
ohmyzsh/ohmyzsh path:plugins/git # β the OMZ git plugin, no OMZ install
It works by generating a static, pre-compiled load script. Startup sources one flat
file instead of walking 20 git repos. It also supports kind:defer (via zsh-defer) to
load plugins after the prompt paints, and kind:fpath for completions that should go on
fpath without being sourced at all.
brew install antidote
Bundle file β ~/.zsh_plugins.txt (zsh_plugins.txt in this gist is a working one).
zstyle ':antidote:bundle' use-friendly-names 'yes'
antidote_dir="$(brew --prefix)/opt/antidote/share/antidote"
[[ -e $antidote_dir/antidote.zsh ]] && source $antidote_dir/antidote.zsh
zsh_plugins=${ZDOTDIR:-$HOME}/.zsh_plugins
if [[ ! ${zsh_plugins}.zsh -nt ${zsh_plugins}.txt ]]; then
antidote bundle <${zsh_plugins}.txt >|${zsh_plugins}.zsh
fi
source ${zsh_plugins}.zsh
autoload -Uz compinit && compinit -C # -C skips the security check: much faster
eval "$(starship init zsh)"
eval "$(fnm env --use-on-cd --shell zsh --version-file-strategy=recursive --resolve-engines)"
eval "$(zoxide init zsh)"
eval "$(direnv hook zsh)"
eval "$(atuin init zsh --disable-up-arrow)" # keeps β for history-substring-search
source <(fzf --zsh)
ZSH_AUTOSUGGEST_STRATEGY=(history completion)
ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=20
zsh-syntax-highlightingmust be last in the bundle. It wraps every widget defined before it; anything loaded afterwards simply isn't highlighted.zsh-history-substring-searchcomesafter syntax-highlighting.ohmyzsh/ohmyzsh path:lib/*comesbefore anypath:plugins/*that depends on it.- Everything that hooks
precmdor rebinds keys β starship, fnm, zoxide, atuin, fzf, direnv β goes in.zshrcaftersource ${zsh_plugins}.zsh.
brew install starship # config β ~/.config/starship.toml
starship.toml in this gist is a lean two-line prompt. The setting that matters most:
command_timeout = 200 # default is 500ms β long enough to feel broken
Any module slower than that is dropped for that render rather than stalling your shell. The config also turns prod Kubernetes contexts red, on the same reasoning that root's prompt is red: make the state you can regret visually unmistakable.
Prefer Powerlevel10k? p10k.zsh in this gist is a full hand-written config with instant
prompt and transient prompt. Pick exactly one β running both wastes startup and only
one can own the prompt.
antidote's whole point is that you stop paying for plugins you aren't using:
- Measure :
hyperfine --warmup 3 'zsh -i -c exit'.Target < 100ms β antidote should beat an OMZ setup comfortably. If it doesn't, something in.zshrcis doing real work. - Profile :
zmodload zsh/zprofas line 1 of.zshrc,zprofas the last line. - Defer anything non-essential: append
kind:deferin the bundle file. compinit -Cskips the daily security audit offpath. Worth 20β70ms. Drop the-Cif you install completions from untrusted sources.
| Conflict | Resolution |
|---|---|
OMZ nvm /pyenv plugins vsfnm/uv |
don't bundle them β see PHASE 5 |
| OMZ theme vs starship | no OMZ theme exists here; starship owns the prompt |
atuin vshistory-substring-search |
atuin init zsh --disable-up-arrow |
OMZ fzf plugin vsfzf --zsh |
pick one, or you double-bind Ctrl-R |
OMZ ssh-agent vs1Password agent |
don't bundle it, use IdentityAgent (PHASE 8) |
aws /kubectl /terraform OMZ plugins |
shell out at load; add kind:defer or skip |
You still get OMZ's ~150 git aliases (gst, gco, gp) β and they are still
interactive-only. Your Bash tool, CI, and any bash -c will not see them. PHASE 4's
rule is unchanged: anything an agent must call has to be a real executable in
~/.local/bin.
Put this in ~/.claude/CLAUDE.md so the model doesn't guess wrong:
Shell is zsh + antidote Oh My Zsh plugins. Aliases (gst, gco, k, β¦) exist
only in interactive shells β always use full commands (git status) in tool calls.
Install via Brewfile. For each, wire up the shell integration and verify it:
| Replaces | Tool | Why it earns its place |
|---|---|---|
cat |
bat | syntax highlight + git gutter; --style=plain for piping |
ls |
eza | git status column, tree mode, sane colors |
grep |
ripgrep | 10β100Γ faster, respects .gitignore by default |
find |
fd | sane syntax, parallel, gitignore-aware |
cd |
zoxide | frecency jumps: z proj |
du |
dust /df βduf |
actually readable |
ps /top |
procs ,btop | modern output |
sed /awk |
sd ,choose | for the 90% case; keep the originals |
diff |
delta | wire into git.pager , not just standalone |
| history | atuin | SQLite history, sync, Ctrl-R that actually works |
| β | fzf | the connective tissue; install fzf --zsh keybindings |
| β | jq +yq +gron | JSON/YAML surgery; gron makes JSON greppable |
| β | hyperfine ,tokei ,watchexec | benchmark, count, re-run on change |
| β | gh ,lazygit ,git-absorb | GitHub CLI, TUI git, auto-fixup commits |
| β | httpie orxh ,curlie | HTTP without flag archaeology |
| β | tealdeer (tldr ) |
examples instead of man pages |
| β | direnv (or mise's env) | per-project env vars |
If installing only two: fzf + zoxide. They remove the most daily friction.
This is what turns a pretty terminal into one an AI agent can actually drive.
Aliases don't exist for tools. A non-interactive shell (bash -c "..." , CI, an
agent's Bash tool) does not source your interactive aliases. So:Anything you want both you and an agent to call must be areal executable in
~/.local/bin , not an alias or an interactive-only function.Write them as small #!/usr/bin/env bash scripts withset -euo pipefail , and keep
aliases only for pure typing shortcuts.
2.
Guard the interactive-only parts so non-interactive shells stay fast and quiet:
[[ -o interactive ]] || return # top of the interactive-only section
Split config: ~/.zshenv (env, always),~/.zprofile (login/PATH),~/.zshrc (interactive).
3.
Kill every pager. A pager in a non-TTY is a hung tool call:
export PAGER=cat GH_PAGER=cat
export LESS='-FRX'
git config --global core.pager 'delta --paging=never'
Also export HOMEBREW_NO_ENV_HINTS=1 and setGIT_TERMINAL_PROMPT=0 so git fails
fast instead of blocking on a credential prompt.
4.
Non-interactive by default.alias -free wrappers should never prompt. Preferbrew install -q ,npm ci --no-fund --no-audit ,gh --json . Anything that might
open$EDITOR needs--no-edit orEDITOR=true .
5.
Structured output where it exists. Teach the setup (and CLAUDE.md) to reach forgh ... --json ,docker ... --format json ,jq ,sd ,rg --json . Agents parse
JSON reliably and scrape human tables badly.
6.
Safety rails that survive an agent typo:
brew install trash; makermininteractive zsh a wrapper aroundtrash, and leave realrmfor scripts.git config --global rerere.enabled true,push.default simple,pull.rebase true,init.defaultBranch main,fetch.prune true.- Set up
git commit --no-gpg-signfallback if signing would prompt for a passphrase in a non-TTY (or use an SSH-agent-backed signing key that doesn't prompt).
.inputrc / zsh keybindings for real line editing:bindkey -e , word-wiseAlt-β/β ,Ctrl-x Ctrl-e to open the current command in$EDITOR .
The 2026 shift is away from a single polyglot manager toward fast, native, per-language
tools β plus the language's own built-in mechanism where one now exists. Use mise only
for the long tail.
| Language | Tool | Why this one |
|---|---|---|
| Node | fnm |
Rust; ~40Γ faster than nvm; auto-switches on cd |
| Python | uv |
replaces pip + virtualenv + poetry + pyenv , all in one |
| Rust | rustup |
canonical, no alternative worth considering |
| Go | (built in) | GOTOOLCHAIN +go.mod β no third-party manager needed |
| everything else | mise |
Java, Ruby, Erlang, Deno, Bun, CLI tools |
brew install fnm
fnm install --lts && fnm default lts-latest
eval "$(fnm env --use-on-cd --shell zsh --version-file-strategy=recursive --resolve-engines)"
--use-on-cdswitches version automatically when you enter a project.--version-file-strategy=recursivelooks in parent dirs, so it works from a subdirectory of a monorepo, not just the root.--resolve-engineshonours"engines": {"node": "..."}inpackage.json.- Reads both
.nvmrcand.node-version, so it's a drop-in for an nvm team.
Corepack gotcha (new in 2026): Corepack was removed from Node 25+ in January 2026.
If you rely on packageManager in package.json to pin pnpm/yarn, install it yourself:
npm install -g corepack. fnm --corepack-enabled only helps on versions that still
bundle it. Silent breakage on a fresh machine otherwise.
uv manages interpreters as well as packages β pyenv is redundant.
brew install uv
uv python install 3.13 3.12 # interpreters β ~/.local/share/uv/python/
uv python pin 3.13 # writes .python-version for this project
Daily use: uv venv, uv sync, uv run <script>, uv add <pkg>, uv tool install <cli>,
uvx <cli> for one-off runs. Never pip install --user, and never install into system
Python β uv's interpreters are isolated from it by design.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path
rustup component add clippy rustfmt rust-analyzer
Use rustup, not brew install rust β brew's is a fixed version with no toolchain
switching, and it collides with rustup if both are on PATH. Per-project pinning is
rust-toolchain.toml, which rustup honours automatically.
Since Go 1.21 the toolchain manages itself. There is no reason to run goenv or g:
brew install go
export GOTOOLCHAIN=auto # in ~/.zshenv
A go.mod with a toolchain directive causes the go command to download and use that
exact toolchain automatically. Everyone building the module gets the same compiler with
no version-manager coordination β the "works on my machine" class of Go bug is gone.
go 1.24 // minimum version required to build
toolchain go1.25.1 // preferred toolchain, fetched on demand
GOTOOLCHAIN=auto opts into that. GOTOOLCHAIN=local disables it and pins you to whatever
brew installed β which is what you want in a hermetic CI image, not on a laptop.
brew install mise
mise use -g java@temurin-21 ruby@3.4 deno@latest
eval "$(mise activate zsh)"
Keep mise out of Node/Python/Rust/Go. Two managers racing for the same PATH entry is a
genuinely miserable class of bug β you get a version that depends on the order your .zshrc
happened to eval. One language, one owner.
git config --global init.defaultBranch main
git config --global pull.rebase true
git config --global push.default simple
git config --global fetch.prune true
git config --global rerere.enabled true # remember conflict resolutions
git config --global core.pager 'delta --paging=never'
Whatever the user names. If Neovim, start from LazyVim or kickstart rather than hand-rolling
a config, and set EDITOR/ VISUAL consistently.
The one decision in this document you should not default. Ask, then set it up properly.
For an AI-native workstation the axis that matters isn't syntax highlighting or plugin count β it's how Claude Code connects to the editor. Claude Code ships an IDE protocol (a local WebSocket speaking MCP) that gives the agent your selection, open files, and diagnostics, and renders diffs inline instead of in the terminal. Support tiers differ:
| Editor | Claude Code integration | Trade |
|---|---|---|
| VS Code + forks (Cursor, Windsurf, Kiro) | official extension | biggest ecosystem; Electron, so heaviest |
| JetBrains (IntelliJ, PyCharm, WebStorm, GoLand, β¦) | official plugin | best refactoring/debugger in class; slow start, RAM-hungry |
| Zed | evolving β native support in progress; CLI works today | fastest editor here, Rust, great collab |
| Neovim | community plugin implementing the same WebSocket protocol | lowest latency, fully terminal-native; you own the config |
| anything + a terminal | the CLI itself, always | zero integration work, no inline diffs |
Nobody is locked out. The CLI works in any terminal, including the built-in terminal of an editor with no extension at all. The extension buys inline diffs and automatic context β not access.
claude --ideauto-connects on startup when exactly one valid IDE is available. Live connections appear as lock files in~/.claude/ide/. If the agent doesn't seem to see your open file, check that directory before debugging anything else.- The extension and the CLI share conversation history. Start in the editor, continue in
the terminal with
claude --resume, or the reverse. You are not picking a silo β so optimise the choice for how you like toedit , not for where the agent lives.
- Already in JetBrains β stay . The plugin is first-class; nothing below beats its debugger or refactoring, and switching editors to chase an agent is a bad trade.
- Wants maximum AI integration β Cursor (VS Code fork, so the Claude Code extension works, plus its own agent). Accept the Electron footprint.
- Wants speed and a native feel β Zed . Genuinely faster than VS Code; run Claude Code in its terminal until native support lands.
- Terminal purist β Neovim with LazyVim or kickstart. Donot hand-roll from an empty
init.luaon a machine you need working today. - Undecided β VS Code . Largest ecosystem, official extension, easiest to leave.
This is where PHASE 4 and this phase intersect, and it's easy to get subtly wrong.
export EDITOR="code --wait" # or: "cursor --wait", "zed --wait", "nvim"
export VISUAL="$EDITOR"
--wait is required for GUI editors: without it the command returns instantly, git sees
an empty buffer and aborts your commit. But the same flag is a hang in a non-interactive
shell β the agent's tool call blocks forever on a window nobody will close. So:
[[ -o interactive ]] || export GIT_EDITOR=true
Pair that with GIT_TERMINAL_PROMPT=0 from PHASE 4 and git fails fast in tool calls instead
of blocking. A terminal editor (nvim, vim) doesn't need --wait and doesn't have this
problem β one more quiet point in favour of the terminal-native option.
-
The CLI launcher (
code,cursor,zed) so$EDITORresolves β VS Code needsShell Command: Install 'code' command in PATH from the command palette. -
Your language servers via the editor, not duplicated with the ones in PHASE 6.
-
ApplePressAndHoldEnabled -bool falsefrom PHASE 7 β without it, holding a key in Vim mode shows the accent-picker instead of repeating. Already in this document; it matters most here.
claude CLI installed and authenticated (claude doctor must pass β show its output). #
~/.claude/CLAUDE.md β global operating instructions. Keep it short andspecific ;
it's loaded into every session, so every line costs context. Good content:
- which package manager/toolchain this machine uses (
uv, notpip;pnpm, notnpm) - "prefer
rg/fdovergrep/find", "usegh --jsonfor GitHub" - project layout conventions (
~/Developer/<org>/<repo>) - the user's commit-message and PR conventions
- what NOT to do on this machine (e.g. never push to
main, neverbrew upgradeunprompted) - which package manager/toolchain this machine uses (
~/.claude/settings.json β the permission allowlist is the single highest-leverage
thing for a terminal power user. Allowlist the read-only commands you run constantly so
you stop approving them:rg ,fd ,eza ,bat ,git status/diff/log ,gh pr view ,jq ,ls ,cat ,tokei . Deny-list the destructive ones outright. #
Hooks for the policy layer that shouldn't depend on the model remembering:
format-on-edit (PostToolUse ), blockrm -rf / andgit push --force on protected
branches (PreToolUse ), desktop notification onStop . #
MCP servers β add narrowly, they cost context. The 80% stack:Context7 (live
library docs),GitHub ,Playwright (browser). Add Postgres/Sentry/Linear only if
the user actually uses them daily. Prefer project-scoped.mcp.json over global. #
Skills and subagents in~/.claude/skills/ for repeatable multi-step workflows. #
Optional second opinion: keep one alternate agent CLI installed for cross-checking, and ollama if the user wants local models for offline/private work. #
Status line : show model, cwd, git branch, and context remaining. #
claude() shell wrapper β seeclaude-wrapper.zsh in this gist. Paste into~/.zshrc .
It solves three things the bare CLI doesn't:
- Two credentials, one switch. Holds both an
ANTHROPIC_API_KEYand aCLAUDE_CODE_OAUTH_TOKEN;claude-api/claude-oauth/claude-use <mode>pick between them. API key = pay-per-token console billing; OAuth token = your Claude subscription. Which one you're on is a billing decision, so make it explicit. - Mutual exclusion. If both variables are visible to the CLI its choice is
ambiguous β you get confusing 401s or bill the wrong account. The wrapper runs in a
subshell that
unsets the one it isn't using, so exactly one ever reachesclaude.Do not implement this asenv -u OTHER_VAR VAR=x command claude.envexecs an external program, so it cannot invoke zsh'scommandbuiltin β it either fails with "command not found" or silently runs some unrelated/usr/local/bin/commandon your PATH. A subshell keeps the builtin and confines theunset. - Default flags, safely. Injects
--dangerously-skip-permissions, but suppresses it for subcommands that reject it (mcp,doctor,setup-token,config, β¦), as root, when you already passed a permission flag, and wheneverCLAUDE_SAFE=1is set. This means everyclauderun can edit files and execute commands without asking. That is the point of the flag, but it is a real trade: useclaude-safein any directory you don't fully trust, and keep thedenylist insettings.jsonpopulated regardless.
Apply with defaults write, then killall Finder Dock SystemUIServer. Confirm the list
with the user first β these change muscle memory:
defaults write NSGlobalDomain KeyRepeat -int 2
defaults write NSGlobalDomain InitialKeyRepeat -int 15
defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool false # key repeat in editors
defaults write NSGlobalDomain AppleShowAllExtensions -bool true
defaults write com.apple.finder AppleShowAllFiles -bool true
defaults write com.apple.finder ShowPathbar -bool true
defaults write com.apple.finder FXPreferredViewStyle -string "Nlsv"
defaults write com.apple.dock autohide-time-modifier -float 0.15
defaults write com.apple.dock show-recents -bool false
defaults write com.apple.screencapture location -string "$HOME/Screenshots"
defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true
Apps worth the slot: Raycast (replaces Spotlight; scriptable, has a Claude/AI layer), AeroSpace or Rectangle (tiling/window management), Karabiner-Elements (Caps Lock β Ctrl/Esc β the single best keyboard change for a terminal user), Hammerspoon if they like Lua automation.
- SSH key :
ssh-keygen -t ed25519 -C "<email>", add to keychain via~/.ssh/config(UseKeychain yes,AddKeysToAgent yes,IdentityFile ~/.ssh/id_ed25519). - Prefer a 1Password (or Secretive) SSH agent so keys live in the Secure Enclave and
never touch disk unencrypted. Set
IdentityAgentaccordingly. - Sign commits with the SSH key , not GPG β far less friction:
git config --global gpg.format ssh+user.signingkey+commit.gpgsign true. - Secrets in the shell: never literal values in dotfiles. Use
op read op://vault/item/fieldat call time, or macOS keychain (security find-generic-password -w -s <svc>). A dotfiles repo containing a real key is a breach waiting for agit push --public. - Confirm FileVault is on , firewall enabled, and Touch ID works for
sudo(/etc/pam.d/sudo_localβ this needs sudo, so ask first). - Add a pre-commit secret scanner (
gitleaksortrufflehog) as a global git hook.
- Write
~/.local/bin/doctorβ one script that checks every tool resolves, the shell starts under budget,git/gh/ssh -T git@github.comauth works,claude doctorpasses, andfnm/uv/go/rustupeach resolve a version.Run it and paste the full output. - Commit the dotfiles repo (chezmoi/stow/bare) and push it. Include
Brewfile,~/.config/*,~/.claude/, and abootstrap.shthat replays PHASES 1β8 on a fresh Mac.bootstrap.shin this gist is a working starting point β bash 3.2 compatible (macOS ships bash 3.2, so no associative arrays /mapfile/${var,,}),--dry-runcapable, never calls sudo, and prints a "Needs a human" list for the GUI-only steps. - Write a short
SETUP.md: what was installed,what was deliberately skipped and why , what still needs the user's hands (GUI logins, App Store, MDM-blocked items). - Final report format:
-
β Done (with proof)
-
βοΈ Skipped + reason
-
β οΈ Needs a human
-
π
zshstartup ms,brew list | wc -l, disk used -
Installing 80 brew packages "because the gist said so". Every tool is context the user must hold. If they won't use it this month, skip it and note it in SETUP.md.
-
Pasting a 30-line bundle file in by reflex. Every non-deferred entry is sourced on every shell start. Bundle what you use, profile with
zprof,kind:deferor drop the rest. -
Running two version managers for one language (mise and fnm for Node). Whichever
evals last wins thePATH, so your version silently depends on.zshrcline order. -
Curl-pipe-bash from a URL you haven't read.
-
Symlinking a dotfiles repo over existing config without backing up.
-
Claiming a phase is done without output. Run the command. Show the result.
-
Editing
/etc/*or runningsudowithout asking. -
Putting an API key in
.zshrcand then committing.zshrcto a public dotfiles repo. If you keep credentials in shell config (seeclaude-wrapper.zsh),chmod 600it and split the secrets into a gitignored~/.claude-auth.zshthat.zshrcsources.
If you're a fresh agent picking this up: cat ~/.setup-state.json, then continue from
phase. Re-running completed phases must be harmless β that's what RULE 1 is for.
- Ghostty + Starship modern terminal setup β https://alexanderkey.com/blog/how-to-set-up-a-modern-terminal-with-ghostty-zsh-and-starship/
- The Ultimate Terminal Stack in 2026 β https://medium.com/vmacwrites/the-ultimate-terminal-stack-in-2026-a-cross-platform-guide-for-macos-linux-and-windows-c0d1f93cd9cc
- Modern Rust CLI tools (eza, bat, fd, zoxideβ¦) β https://32blog.com/en/cli/cli-modern-rust-tools
- antidote β https://antidote.sh/ Β· bundle annotations:https://github.com/mattmc3/antidote
- Oh My Zsh plugin directory (loadable via antidote) β https://github.com/ohmyzsh/ohmyzsh/wiki/Plugins
- starship config reference β https://starship.rs/config/
- fnm β https://github.com/Schniz/fnm
- uv: managing Python versions β https://docs.astral.sh/uv/concepts/python-versions/
- Go toolchains (GOTOOLCHAIN) β https://go.dev/doc/toolchain
- Claude Code IDE integrations β https://developertoolkit.ai/en/claude-code/quick-start/ide-integration/
- Zed vs every editor β https://zed.dev/compare
- Powerlevel10k (alternative prompt) β https://github.com/romkatv/powerlevel10k
- mise (for the long tail) β https://mise.jdx.dev/
- Claude Code + MCP guide 2026 β https://www.jitendrazaa.com/blog/ai/claude-code-complete-guide-2026-from-basics-to-advanced-mcp-2/
- Best MCP servers for Claude Code 2026 β https://evomap.ai/blog/best-mcp-servers-for-claude-code-2026