Personal-first, idempotent fresh-Mac setup. One command on a brand-new macOS install produces a working workstation; reruns pick up new tools as the lists evolve.
On a fresh Mac, download the bootstrap script and run it:
curl -fsSL https://raw.githubusercontent.com/pjungermann/mac-setup/main/mac-setup.sh -o mac-setup.sh
chmod +x mac-setup.sh
./mac-setup.shThe script fetches the full repo as a tarball (no git required yet),
re-execs from ~/mac-setup/, then after Homebrew installs git it
converts that directory into a real clone so later reruns are just:
cd ~/mac-setup && git pull && ./mac-setup.shmac-setup.sh is a thin router that dispatches to a matching script
under bin/. The default (no subcommand) is install-software, so the
fresh-Mac flow above is unchanged.
./mac-setup.sh # defaults to install-software
./mac-setup.sh install-software # bin/install-software.sh
./mac-setup.sh migrate-laptop <args> # bin/migrate-laptop.sh
./mac-setup.sh migrate-claude-code <args> # bin/migrate-claude-code.sh
./mac-setup.sh --helpEach subcommand script is also directly invokable from bin/.
Provisions a fresh macOS install from scratch to a working personal
workstation. Idempotent; reruns pick up anything you have added to the
Brewfile, dotfile templates, or config/notes.yaml since the last run.
In order:
- Xcode Command Line Tools, installed via
xcode-select --installif missing. - Homebrew, via the official installer, with
shellenvwired into~/.zprofile. - Pre-install notes.
bin/notes.sh --phase prereqsurfaces anything you must prepare up front (keys, profiles, SSO). - Homebrew packages.
bin/brew-bundle.shrunsbrew bundleon the trackedBrewfileplus optionalBrewfile.local, skipping any cask whose.appis already present but not brew-managed (handles MDM-pushed apps transparently). - Repo-as-clone. The bootstrapped
~/mac-setup/becomes a real git clone of this repo now that git exists. - chezmoi applies dotfiles (
dot_zshrc.tmpl,dot_gitconfig.tmpl,dot_config/mise/config.toml) from this repo as the chezmoi source. Arun_once_before_script installs Oh My Zsh first. - Default shell.
chsh -s /bin/zshruns if your login shell isn't already zsh. - mise runtimes.
mise installpicks up~/.config/mise/config.toml. - Yarn, enabled via Corepack; per-project versions pinned via each project's
packageManagerfield. - Claude Code CLI, installed from Anthropic's official script.
- Post-install notes.
bin/notes.sh --phase postprints per-app follow-ups (profile restores, SSO sign-ins, interactive first launches).
Bundles the irreplaceable bits a fresh install-software run cannot
regenerate, carries them across a laptop switch, and surveys the source
machine beforehand so you know what will travel. Deliberately does not
carry auth tokens or cloud configs: aws sso login, gh auth login,
docker login <registry>, op signin, cargo login, and package
registry CLIs each regenerate theirs on first use, and travelling them
widens the attack surface for no real time saving.
Commands:
scan. Read-only report of what would travel. Homebrew leaves and casks, mas apps, SSH and GPG keys, workspace repos (with highlights for anything with uncommitted or unpushed work), cloud / CLI auth that will need re-logging on the new laptop, and a Brewfile delta against the Brewfile in the mac-setup clone this script runs from.export [--dry-run] [--yes] [--encrypt] [outfile]. Writes a tar.gz plus.sha256sidecar with SSH keys and config, GPG keyring exports (public and secret keys plus ownertrust), shell history,*.localdotfile overlays, and the scan reports. The bundle carries real secrets;--encryptstreams the tar throughgpg -c(AES-256 symmetric) so the plaintext tar never touches disk.install [--dry-run] <tarball>. On the new laptop. Verifies the SHA256 fail-closed and extracts into$HOME. Any pre-existing collision (for example~/.ssh) is moved to a timestamped.backup-<stamp>/sibling before the bundled copy is placed, so first-bootknown_hostsentries you accumulated between fresh-OS and restore are preserved in the backup rather than merged.verify [--root <path>]. Lists what the restore placed and what is still missing.
Backs up and restores ~/.claude state (CLAUDE.md, guidance, skills,
hooks, plugin data, session transcripts, prompt history). Never invoked
by install-software; the user runs it on demand, for recurring
backups, rollback after a bad config change, or carrying Claude Code
state to a new machine. Session transcripts can hold anything you or
Claude handled during a session, so treat the resulting tarball as
sensitive (use --encrypt, do not leave it on shared storage, delete
after restore).
Commands:
backup [--yes] [--encrypt] [outfile]. Writes a tar.gz plus.sha256sidecar. Drift-detects entries under~/.claude/that are not in the include or exclude catalog and prompts before continuing (--yesskips the prompt).--encryptstreams the tar throughgpg -c(AES-256 symmetric).restore [--dry-run] <tarball>. Verifies the SHA256 fail-closed and extracts over~/.claude/. Any existing directory is first copied to~/.claude.backup-<stamp>/so a bad restore is reversible. Machine-local state outside the include catalog (caches, daemon logs, downloads) is left alone.verify [--root <path>]. Checks the extracted state and lists plugin counts plus MCP integrations found in enabled plugins. MCP tokens are not transferred; re-authenticate via the plugin's own flow on first use.list. Lists project-memory directories under~/.claude/projects/.
export and install remain as back-compat aliases for backup and
restore.
See AGENTS.md. Highlights:
- Package manager priority: mise > brew > mas.
- Public repo, personal-first; no company references in tracked files.
~/.claude/is not touched by theinstall-softwareflow; use./mac-setup.sh migrate-claude-code(orbin/migrate-claude-code.sh) to back it up or restore it.- All shell scripts target macOS bash 3.2 and must pass shellcheck.
- Commits follow Conventional Commits; CI runs commitlint on PR commits.
The repo is personal-first but structured for forks:
- Edit
Brewfile,dot_*templates, andconfig/notes.yamlto match your own setup. - Keep private or work-only entries in
Brewfile.local(gitignored); seeexamples/Brewfile.local. - Work-specific git identity goes in a separate overlay included from
~/.gitconfig; seeexamples/gitconfig-work-include.