Setting up a new MacBook for AI-assisted development
An opinionated walk through provisioning a new Apple Silicon MacBook for fullstack TypeScript work — what to install, what to skip, and the handful of choices that actually shape day-to-day productivity.
5 min read · New · 👍 0
A new machine is the rare moment when you can do the foundation right without unwinding the wrong version of every choice from the last one. This entry is a reference for that moment: what to put on an Apple Silicon MacBook before you start shipping, and which choices to actually think about versus copy-paste.
The setup below is opinionated for AI-assisted fullstack TypeScript work — Claude Code, Cursor, and a heavy terminal habit. Adjust the application list for what you actually use; the foundations (shell, runtimes, dotfiles, SSH) generalise.
#// Pre-flight: don't run your shell under Rosetta
The single decision that determines whether the rest of this setup goes smoothly is whether the terminal app is allowed to translate to x86. If it is, every brew install lands in /usr/local/ (the Intel path), every binary runs through Rosetta on each invocation, and the machine quietly stays half-translated for its entire life. The fix is to confirm "Open using Rosetta" is unchecked on the terminal before the first command runs.
# Verify you're native ARM before installing anything
arch
# Should print: arm64
# If it prints i386, your terminal is running under Rosetta — quit it and uncheck
# the "Open using Rosetta" box in the app's Get Info pane.Install Rosetta itself for the occasional Intel-only app that genuinely needs it, but don't route your shell through it.
// decision
Verify arch before the first brew install
- Install Rosetta and don't worry about it: Defensible only if you actively need x86 binaries — most modern Mac dev workflows do not.
#// System foundations: Xcode CLI tools, then Homebrew
Two installs precede everything else. Xcode Command Line Tools provides the system compiler that Homebrew formulas need; Homebrew is the package manager every other foundation passes through. On Apple Silicon, Homebrew installs to /opt/homebrew and adds itself to PATH only after you opt in via ~/.zprofile.
xcode-select --install
# Wait for the GUI installer to finish before continuing.
/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)"~/.zprofile runs on login shells (every new terminal window); ~/.zshrc runs on every shell. PATH belongs in zprofile so it's set once per session and inherited by every nested shell.
#// Shell: Oh My Zsh + Powerlevel10k via Homebrew
The shell layer is the surface you actually live in. Two pieces matter: a framework that handles completion and plugin loading (Oh My Zsh), and a prompt that gives you the context you need at a glance (Powerlevel10k). Installing P10k via Homebrew rather than the manual git clone keeps it on the upgrade path with everything else.
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
brew install powerlevel10kThen in ~/.zshrc, the top-of-file instant-prompt block runs before Oh My Zsh loads (so the prompt renders immediately while the rest of the shell warms up), the theme line is empty (because P10k handles it), and the bottom of the file sources the theme.
# Top of ~/.zshrc (before Oh My Zsh loads):
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi
ZSH_THEME=""
# Bottom of ~/.zshrc:
source $(brew --prefix)/share/powerlevel10k/powerlevel10k.zsh-theme
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zshThe ~/.p10k.zsh file is where the prompt is actually configured — segments, colours, separators. Copy it from the old machine rather than re-running p10k configure; the customisation accumulates over months and is annoying to redo.
#// Runtimes: asdf manages Node, pnpm, Python per project
A system Node version is the wrong default the moment you touch a second project. asdf (or its newer cousin mise) reads a .tool-versions file in any directory and activates the pinned runtime transparently — cd into the project, the right Node version is on PATH. Pin Node, pnpm, and Python; everything else can be Homebrew.
brew install asdf
echo '. $(brew --prefix)/opt/asdf/libexec/asdf.sh' >> ~/.zshrc
source ~/.zshrc
asdf plugin add nodejs
asdf plugin add pnpm
asdf plugin add python
asdf install nodejs 24.13.0
asdf set --home nodejs 24.13.0
asdf install pnpm 10.28.2
asdf set --home pnpm 10.28.2
asdf install python 3.12.12
asdf set --home python 3.12.12asdf set --home <plugin> <version> writes the version into ~/.tool-versions, making it the system-wide default; per-project pins (a .tool-versions in the repo) override it. Commit the per-project file alongside package.json and tsconfig.json.
#// Tooling: one brew install, opinionated picks
Beyond runtimes, a small set of CLI tools and a handful of GUI apps cover most of what daily fullstack work needs. Batch the brew installs so the whole roster lands in a single network round trip.
brew install \
bat btop eza fzf gh git tmux \
zoxide lazygit jq lychee
brew install --cask \
ghostty raycast rectangle obsidian docker \
cursor visual-studio-code claude
npm install -g @anthropic-ai/claude-code
pnpm add -g vercel turbobat is cat with syntax highlighting; eza is ls with icons and git-awareness; fzf powers the in-shell file picker and feeds tmux/extrakto/zoxide; zoxide replaces cd with a frecency-ranked smart jumper; lazygit is the terminal Git UI that pairs perfectly with the tmux popup binding from the companion skill.
#// SSH and Git: transfer the key, don't regenerate
If you have an existing SSH key, transfer it; don't regenerate. Re-keying means re-adding to GitHub, every server, every deploy target, and any signed-commit verification chain. The key is portable.
# After copying ~/.ssh/ from the old machine:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub ~/.ssh/config
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch mainAddKeysToAgent yes + UseKeychain yes in ~/.ssh/config is the macOS-specific incantation that gets the key loaded from the system keychain on every shell — no passphrase prompt per terminal window.
#// What to install second, or never
Resist the temptation to mirror the old machine exactly. A new machine is a free chance to drop the four apps you opened three times last year. The accumulating tax of "but I might need it" is real — every install adds login items, menu bar icons, and background updaters that drain the resource budget you were trying to reclaim.
Things worth deferring until a specific project demands them: Docker Desktop (heavy; only if you actually run containers locally), JetBrains tooling (only if a project mandates it), Adobe Creative Cloud (only if you're producing assets this week). The principle: a fresh machine should reach "I can ship" with the smallest possible install set, then grow exactly the surface each new project pulls in.
The companion artifact ships the full setup as an executable script. Run it phase-by-phase on a fresh machine; comment out anything you don't need. The dotfiles directory it lays down (~/.zshrc, ~/.gitconfig, ~/.ssh/config template) is opinionated but easy to overwrite section-by-section.
// decisions
Native ARM shell over Rosetta-translated x86
Running the terminal under Rosetta on Apple Silicon is the most common machine-setup mistake — it lands Homebrew at /usr/local instead of /opt/homebrew and quietly translates every CLI binary on every invocation. The fix is one Get Info checkbox before the first brew install, and the cost of getting it wrong is a full reinstall later.
asdf (or mise) for Node/Python/pnpm instead of system installers
Different projects pin different runtime versions; a system Node makes the wrong version the default for every shell. A version manager reads .tool-versions per directory and switches transparently, which keeps the same machine usable across years of pinned legacy projects and bleeding-edge new ones.