Skip to content

Repository files navigation

mac-bootstrap

One command. Zero to a fully armed and operational macOS workstation. Shell, toolchains, dotfiles, auth, browser automation, LLM coding agents — all of it.

If you're the kind of engineer who treats their machine like a cattle, not a pet, this is your herd script.

!!! tip On WSL2 / Ubuntu? The script detects non-macOS and skips the Apple-specific stuff automatically — no Xcode CLT, no defaults, no casks. Everything else (Homebrew, zsh + Zim, mise, tools, dotfiles) runs the same. See Brewfile.linux for the Linux package list.


Before you run this

  • Apple ID — signed into the App Store (for mas to pull apps)
  • A functioning brain — the script's interactive. It'll ask you things. It won't hold your hand for sudo.

1Password is optional. If you set your vault name in config.env, the bootstrap pulls API keys into a private zsh file. If you don't, everything else still works — you'll just need to configure API keys yourself.


Make It Yours

Fork the repo, edit config.env with your values, then run:

git clone https://github.com/YOU/mac-bootstrap.git ~/Developer/github/YOU/mac-bootstrap
cd ~/Developer/github/YOU/mac-bootstrap
# edit config.env with your values
./bootstrap.sh
Variable What it does
OP_VAULT 1Password vault name (leave empty to skip API key setup)
GIT_NAME Your name for git config --global user.name
GIT_EMAIL Your email for git config --global user.email
REPO_URL URL of your fork
REPO_DIR Where to clone your fork
OPENCODE_CONFIG_REPO Private opencode config repo, e.g. you/opencode_config (leave empty to skip)
OPENCODE_SKILLS_REPO Public agent skills repo (leave empty to skip)
OPENCODE_WEB_ITEM 1Password login item holding the web UI password (leave empty to skip)
OPENCODE_WEB_PORT Fixed web UI port so the phone URL stays stable
PI_CONFIG_REPO Private Pi config repo, e.g. you/.pi (leave empty to skip)

Then run:

./bootstrap.sh

Run it

The quick and dirty way (runs with defaults, no 1Password):

curl -fsSL https://github.com/ghraw/acastro2/mac-bootstrap/main/bootstrap.sh | bash

That's it. Copy. Paste. Hit enter. Walk away for 10 minutes.

If you'd rather inspect before you yeet:

git clone https://github.com/acastro2/mac-bootstrap.git ~/Developer/github/acastro2/mac-bootstrap
cd ~/Developer/github/acastro2/mac-bootstrap
./bootstrap.sh

Re-running is safe — everything's idempotent. Use --only or --skip to be surgical:

./bootstrap.sh --only=packages,zsh        # just packages + shell retrofit
./bootstrap.sh --skip=macos-defaults,mise,auth  # skip opinionated macOS defaults, toolchains, and auth

The packages phase runs brew bundle --no-upgrade on purpose: a cask desynced by its own updater (VSCode Insiders self-updates) can't fail the run. When you actually want to upgrade, run brew upgrade yourself.

Gatable sections: xcode, brew, repo, workspace, macos-defaults, packages, app-clis, zsh, shell, git, ssh, herdr, opencode, opencode-web, cortex, tgrep, mise, browser-automation, dotfiles, secrets, auth, doctor.


Step into the Madness

Here's the deal: this isn't a generic dotfiles repo. It's the exact setup of a platform engineer who spends their days knee-deep in Terraform, Kubernetes, AWS, and LLM-powered coding agents. Every choice here has a body count behind it.

The OS

macOS. I've run Linux on the desktop. I've tried WSL2. For platform work that involves talking to every cloud, running containers locally, and needing a terminal that doesn't fight you — macOS is the least-worst option. The macos-defaults section sets key repeat to fast, hides the Dock, disables .DS_Store on network volumes, and goes dark mode with an orange accent. Fight me.

The shell

Zsh + Zim. macOS ships zsh; Zim (zimfw) adds the layers on top: sane options, keybindings, cached completion, Fish-like syntax highlighting, history search, and autosuggestions. The ~/.zimrc module list is short. Fzf binds fuzzy Ctrl-R history search, Ctrl-T file insert, and Alt-C directory jump. Starship renders the prompt; Carapace supplies structured completions for hundreds of CLIs, bridged into zsh's completion system.

The Git shortcuts started from commands actually used in Fish history and now include a curated subset of the Oh My Zsh git plugin and Fish plugin-git: staging, diffs, branch views, log graphs, rebase/cherry-pick flows, stash, submodules, switch, and worktrees. Dynamic helpers include grt (repository root) and gpsup (push the current branch with upstream tracking). git-clean fetches and prunes origin, detects its default branch, and safely deletes local branches already merged into it with git branch -d.

The same research added practical Oh My Zsh-style aliases for Docker/Colima, Kubernetes, Helm, AWS, and Terraform/OpenTofu. The upstream references are Oh My Zsh's Git, Docker, Kubernetes, Helm, Terraform, Fish plugin-git, fzf.fish, and Oh My Fish bang-bang. The shortcuts intentionally leave out force-push, hard-reset/pristine, mass-prune/delete, auto-approve, and !!/!$ behaviors.

zsh clears inherited AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, and AWS_PROFILE values at login. Authenticate with an explicit SSO profile and scope AWS_PROFILE to a single command (AWS_PROFILE=work tofu plan); do not leave AWS credentials or account selection in the shell-wide environment.

On a machine that still runs Nushell, the migration is fail-safe: the zsh config is rendered and validated first, then the login shell is switched and read back. Nushell is uninstalled and its configs wiped only after that exact readback succeeds.

The terminal

Ghostty. Native, GPU-accelerated, zero config to look good. It replaced iTerm2, Kitty, and WezTerm for me. The dotfiles include a Ghostty config that just works.

The editor(s)

VS Code Insiders and Zed. I live in VS Code for heavy platform work (Terraform, Go, Kubernetes manifests). Zed for quick edits, markdown, and when I want something that opens before I finish blinking. Both get CLI launchers — code and zed from anywhere.

The package manager

Homebrew with a Brewfile. The list includes git, Carapace, Starship, mise, chezmoi, uv, fzf, ripgrep, bat, eza, neovim, jq, yq, kubectl, helm, k9s, awscli, the Grafana gcx CLI, Azure CLI, colima, Docker Compose, postgresql, redis, 1password, ghostty, VS Code, Zed, and the terminal fonts. Declarative. Boring. Works.

The toolchain manager

mise. Not asdf. Not nvm + pyenv + tfenv. mise is faster, supports piped installs (npm:@playwright/test), and doesn't require shims. It pins Go (latest), Node (LTS), pnpm 10, Python 3.12, .NET 9, OpenTofu, the Playwright CLI, and the Context7 CLI globally. Per-project overrides go in .mise.toml or .tool-versions.

The secret sauce (optional)

1Password CLI → private zsh file. If you set OP_VAULT in config.env, every API key gets pulled from your 1Password vault into ~/.config/zsh/.api-keys.zsh (chmod 600) as export lines. .zshrc sources it, so the keys become environment variables at shell startup.

Skip it if you want — you'll just set up API keys yourself.

Keys managed this way: opencode, anthropic, openai, context7, devto, oreilly, google, resend.

The dotfiles

chezmoi with the source directory inside this repo (home/). It ships .zshrc and .zimrc, one layout for macOS and Linux. Private files live outside the repo: bootstrap creates ~/.config/zsh/.api-keys.zsh (secrets) and ~/.config/zsh/local.zsh (machine-only overrides, sourced last). Ghostty settings live here too. chezmoi diff previews changes; chezmoi apply deploys them.

The LLM coding agent

OpenCode and Pi. OpenCode is installed via its official install script. Pi and the Context7 documentation CLI (ctx7) are installed from their official npm packages through mise. Pi's optional private config repo (PI_CONFIG_REPO) is synced into ~/.pi. An optional private OpenCode config repo (OPENCODE_CONFIG_REPO) gets cloned into ~/.config/opencode with your provider and model config. A public skills repo (OPENCODE_SKILLS_REPO) lands in ~/.agents/skills — these are the alex-skills that teach OpenCode how to write in my voice, review PRs, create diagrams, write ADRs, and handle browser automation.

OpenCode web UI. The web UI is password protected, and that password lives in 1Password (OPENCODE_WEB_ITEM), never in the config repo — the bootstrap reads it and hands it to opencode service set password, so every machine ends up with the same login. When Tailscale is up, the same section binds the server to the tailnet address (OPENCODE_WEB_PORT) so the phone can reach it from anywhere without exposing a port on the local network. With Tailscale down the server stays on localhost, which is also the safe default.

The browser automation stack

Both Node and Python Playwright, plus browser-use (an LLM-driven browser agent). This is how OpenCode's webapp-testing and playwright-cli skills drive a real Chromium to test web apps, take screenshots, and fill forms. The bootstrap also opens System Settings so you can grant Accessibility and Screen Recording permissions — no, it can't grant them for you. macOS is a prison.

The infra tooling

colima for containers (Docker Desktop is a resource hog and the licensing got weird), with Docker Compose installed as a CLI plugin and linked so docker compose works without Docker Desktop. Azure CLI (az) checks Microsoft Entra app registrations and OAuth setup. kubectl, k9s, and helm for cluster work. awscli for, well, AWS. gcx manages Grafana OSS/Enterprise 12+ and Grafana Cloud through their APIs. The shell defaults to Attain's Grafana 13 instance and organization 1. It maps the 1Password service-account token to GRAFANA_TOKEN without storing it in the repo. terraform-linters/tap and tflint because I don't merge Terraform without linting. snowflake-cli and pgcli for data platform work. doggo because dig output makes me sad. herdr for macOS fleet management — version-pinning, drift detection, and one-command setups across machines.

The profile

Who runs this? Someone who:

  • Spends more time in a terminal than Finder
  • Thinks workstations should be disposable and reproducible
  • Doesn't want to remember 12 brew install commands on a fresh machine
  • Ships infrastructure for a living (platform, SRE, DevOps, cloud)
  • Uses AI coding agents and wants them wired into their tools, not bolted on
  • Has strong opinions about prompt rendering speed and won't tolerate lag
  • Doesn't want to touch a Keychain API ever again

If that's you — welcome. Run the command. Break things. Send PRs.


What happens when you run it

The script runs in two phases:

Phase 1 — silent install. Xcode CLT, Homebrew, all packages, validated zsh + Zim config and login-shell check, git config, SSH via 1Password agent, OpenCode, Pi, mise toolchains, Playwright + browser-use, and dotfiles via chezmoi. On a Nushell machine, Nushell is uninstalled only after zsh starts successfully and the account shell readback matches.

Phase 2 — interactive auth. If OP_VAULT is set: 1Password sign-in and API keys written to ~/.config/zsh/.api-keys.zsh. Always: GitHub CLI SSO login, OpenCode and Pi config sync (if set), agent skills clone (if set), and a doctor check that verifies critical binaries are alive.

If anything fails, it tells you what and keeps going. Re-run anytime.


After bootstrap

aws sso login --profile <your-profile>
az login --allow-no-subscriptions  # when checking Microsoft Entra app registrations
AWS_PROFILE=<your-profile> tofu plan
mise install      # if any tools need (re)installing
colima start      # start the Docker runtime when you need containers
docker compose version
# Sign in to desktop apps (Slack, VS Code, etc.)

Open a new terminal. You're in zsh. You're home.


Corporate CA / TLS inspection

The security stack (Cisco Secure Access, Cisco Umbrella, GSA) does TLS inspection, so apps that don't use the macOS keychain need the corp root trusted explicitly. The zsh config activates a bundle automatically when present:

  • Drop the CA bundle at ~/.config/corporate-ca-bundle.pem. When it exists, .zshrc exports NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE, SSL_CERT_FILE, CURL_CA_BUNDLE, PIP_CERT, GIT_SSL_CAINFO, and AWS_CA_BUNDLE to it (covers Node, Python requests/pip, OpenSSL tools, curl, Homebrew git, AWS SDKs). Go 1.27+ and .NET read the macOS keychain, not these vars.

To trust the root at the OS level on one Mac (covers browsers, Go < 1.27, .NET, system git/curl):

sudo security add-trusted-cert -d -r trustRoot -p ssl -k /Library/Keychains/System.keychain ~/.config/cisco-secure-access-root.pem

Run it in your terminal, not headless — macOS shows an auth dialog. Fleet-wide, prefer a Jamf computer-level configuration profile with the Certificate payload (com.apple.security.root, "Allow access to all applications") — that's the Apple-supported way and auto-cleans on profile removal.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages