cade’s dotfiles
Personal dotfiles for macOS and Linux — one command sets up a complete, reproducible dev environment on any machine.
curl -fsSL https://raw.githubusercontent.com/cadebrown/dotfiles/main/bootstrap.sh | bash
What this gives you
- One bootstrap command — installs every tool, dotfile, and language runtime from scratch
- PLAT isolation — compiled binaries live under
~/.local/$(uname -m)-$(uname -s)/, so two machines sharing an NFS home directory never conflict - No sudo on Linux — Homebrew runs inside a rootless container; everything installs to user paths
- Idempotent — every script is safe to re-run; running bootstrap on a second machine just installs that machine’s arch-specific tools
- Single source of truth — one
Brewfilefor both macOS and Linux;if OS.mac?blocks handle the differences automatically - Fast shell startup — lazy nvm loading, single
compinit, ~140ms warm startup
How it works
chezmoi manages dotfiles as templates in home/ and applies them to ~/. The bootstrap script wires everything together:
bootstrap.sh
↓ chezmoi apply dotfiles → ~/
↓ install/zsh.sh oh-my-zsh + plugins
↓ homebrew.sh packages from Brewfile (macOS)
linux-packages.sh packages from Brewfile (Linux, via container)
↓ install/node.sh nvm → Node.js
↓ install/rust.sh rustup → cargo tools
↓ install/python.sh uv → Python venv
↓ install/claude.sh Claude Code + plugins + MCP servers
Compiled tools land under ~/.local/$PLAT/ — a different directory per arch+OS, so a shared home has no conflicts. Text configs (dotfiles) are shared freely; they’re arch-neutral by design.
Sections
Setup — getting started on a new machine
| Page | What it covers |
|---|---|
| Bootstrap | System requirements, what gets installed, platform-specific steps |
| Managing dotfiles | How chezmoi works, editing dotfiles, template variables |
| Package management | Adding tools (Homebrew, cargo, npm, pip), why each layer exists |
Usage — ongoing updates and maintenance
| Page | What it covers |
|---|---|
| Day-to-day workflow | Adding packages, editing dotfiles, deploying docs, updating tools |
| Troubleshooting | Quick reference — when tools aren’t found, builds fail, etc. |
Bootstrap a new machine
One-liner
curl -fsSL https://raw.githubusercontent.com/cadebrown/dotfiles/main/bootstrap.sh | bash
Or from a clone:
git clone https://github.com/cadebrown/dotfiles ~/dotfiles
~/dotfiles/bootstrap.sh
No sudo required on either platform. The script detects the OS and runs the right steps.
macOS
Requirements
| Requirement | How to get it |
|---|---|
| macOS 13+ | — |
| Xcode Command Line Tools | Homebrew prompts automatically, or: xcode-select --install |
| Internet access | — |
What gets installed
- chezmoi →
~/.local/$PLAT/bin/chezmoi - Dotfiles applied via
chezmoi apply— prompts for name + email on first run - oh-my-zsh + plugins (pure prompt, autosuggestions, fast-syntax-highlighting, completions)
- Homebrew →
/opt/homebrew(Apple Silicon) or/usr/local(Intel) - Packages from
packages/Brewfile— CLI tools, casks, macOS services - colima registered as a login service (rootless Docker runtime)
- Node.js via nvm →
~/.local/$PLAT/nvm/ - Rust via rustup →
~/.local/$PLAT/rustup/, cargo tools →~/.local/$PLAT/cargo/ - Python via uv →
~/.local/$PLAT/venv/ - Claude Code via Homebrew cask + plugins + MCP servers
Linux
Requirements
| Requirement | Notes |
|---|---|
| x86_64 or aarch64 | — |
| Docker (rootless) or Podman | Required for the package install step. See below. |
git and curl | Pre-installed on most systems |
| Internet access | — |
No sudo is needed after the initial Docker/Podman setup. Packages install inside a manylinux_2_28 container (AlmaLinux 8, glibc 2.28) — most pour as precompiled bottles; Homebrew bundles its own glibc so the binaries are self-contained on any host.
Docker (rootless)
Rootless Docker runs entirely as your user — no root daemon.
curl -fsSL https://get.docker.com/rootless | sh
export PATH="$HOME/bin:$PATH"
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/docker.sock"
systemctl --user enable --now docker
Full docs: docs.docker.com/engine/security/rootless
Podman (rootless)
Podman is rootless by design — no daemon, no sudo.
apt install podman # Debian/Ubuntu (may need sudo once)
dnf install podman # RHEL/Fedora
which podman # HPC clusters: often already available
The bootstrap script checks for docker first, then podman. Either works.
What gets installed
- chezmoi →
~/.local/$PLAT/bin/chezmoi - Dotfiles applied via
chezmoi apply - oh-my-zsh + plugins
- Homebrew →
~/.local/$PLAT/brew/(inside manylinux container; casks skipped) - Node.js via nvm →
~/.local/$PLAT/nvm/ - Rust via rustup →
~/.local/$PLAT/rustup/,~/.local/$PLAT/cargo/ - Python via uv →
~/.local/$PLAT/venv/ - Claude Code native binary →
~/.local/$PLAT/bin/claude+ plugins + MCP servers
First run takes ~10 minutes (glibc and a few others compile from source). Subsequent runs skip already-installed packages.
Skipping steps
Any step can be skipped with an environment variable:
INSTALL_PACKAGES=0 # skip Homebrew + brew bundle
INSTALL_ZSH=0 # skip oh-my-zsh
INSTALL_NODE=0 # skip nvm + Node.js
INSTALL_RUST=0 # skip rustup + cargo tools
INSTALL_PYTHON=0 # skip uv + venv
INSTALL_CLAUDE=0 # skip Claude Code plugins + MCP servers
Example — skip everything except dotfiles:
INSTALL_PACKAGES=0 INSTALL_ZSH=0 INSTALL_NODE=0 \
INSTALL_RUST=0 INSTALL_PYTHON=0 INSTALL_CLAUDE=0 \
~/dotfiles/bootstrap.sh
First-run prompts
chezmoi asks for display name and email once. Values are cached in ~/.config/chezmoi/chezmoi.toml. To skip the prompts, pre-seed them:
CHEZMOI_NAME="Your Name" CHEZMOI_EMAIL="you@example.com" ~/dotfiles/bootstrap.sh
To re-prompt (e.g. after changing email): chezmoi init --data=false
Shared home directories
If two machines share a home directory (NFS), run bootstrap.sh independently on each:
- chezmoi finds the cached config — no prompts the second time
- Dotfiles are already applied — no changes needed
- All tool installs use
~/.local/$PLAT/, so each machine gets its own arch-specific binaries without conflict
All tools installed here (Homebrew bottles, rustup, uv, nvm) ship self-contained binaries — they don’t link against the host glibc — so different glibc versions on the same arch are not a problem.
Managing dotfiles
chezmoi manages the files in home/ and applies them to ~/, resolving templates along the way.
The quick version
chezmoi edit ~/.zshrc # edit a dotfile (opens in $EDITOR, applies on save)
chezmoi apply # apply all pending changes
chezmoi diff # preview what would change before applying
chezmoi update # git pull + apply (sync from repo)
How files map
Files in home/ map to ~/ by chezmoi’s naming rules:
| Source | Target |
|---|---|
home/dot_zshrc.tmpl | ~/.zshrc |
home/dot_config/git/ignore | ~/.config/git/ignore |
home/dot_ssh/config.tmpl | ~/.ssh/config |
home/dot_claude/CLAUDE.md | ~/.claude/CLAUDE.md |
dot_prefix →.in target.tmplsuffix → rendered as a Go template before writing
Template variables
Use these in any .tmpl file:
{{ .name }} display name (prompted on first run)
{{ .email }} email (prompted on first run)
{{ .chezmoi.os }} "darwin" or "linux"
{{ .chezmoi.arch }} "amd64" or "arm64"
{{ .chezmoi.username }} system login name (auto-detected)
{{ .chezmoi.homeDir }} home directory path
Example — Linux-only alias:
{{ if eq .chezmoi.os "linux" -}}
alias open='xdg-open'
{{ end -}}
Editing dotfiles
Via chezmoi (recommended — auto-applies on save):
chezmoi edit ~/.zshrc
chezmoi edit ~/.zprofile
Directly in the repo (then apply manually):
$EDITOR ~/dotfiles/home/dot_zshrc.tmpl
chezmoi apply
Never edit ~/.zshrc directly — chezmoi will overwrite it on the next apply.
Shared home directory safety
On a shared NFS home, all machines run chezmoi apply against the same target files. Templates must render identically on every machine that shares the home — otherwise machines overwrite each other on every apply.
Rule: never use {{ .chezmoi.arch }} or any per-machine value in a template. Arch-specific logic belongs in shell runtime code instead:
# Good — evaluated at shell startup on each machine independently
export PATH="$HOME/.local/$(uname -m)-$(uname -s)/bin:$PATH"
# Bad — baked into the file at chezmoi apply time; machines fight each other
export PATH="$HOME/.local/{{ .chezmoi.arch }}-{{ .chezmoi.os }}/bin:$PATH"
The existing templates only branch on {{ .chezmoi.os }} (darwin vs linux), which is stable for all machines sharing a home.
Files that other tools also write
Some tracked files are mutated at runtime. chezmoi won’t auto-apply — drift is intentional until you decide what to do:
chezmoi diff # see what changed
chezmoi add ~/.claude/settings.json # pull the live version back into the repo
Notable examples:
~/.claude/settings.json— updated by Claude Code when plugins are installed~/.codex/config.toml— Codex appends project trust levels at runtime; managed withcreate_prefix so chezmoi writes it once and never overwrites
Package management
Every package layer has a declarative text file and an idempotent install script. All scripts skip already-installed items — safe to re-run at any time.
The layers
| Layer | File | Install script | Platform |
|---|---|---|---|
| System packages | packages/Brewfile | install/homebrew.sh / install/linux-packages.sh | macOS (bottles) / Linux (manylinux container) |
| Rust tools | packages/cargo.txt | install/rust.sh | All |
| Python packages | packages/pip.txt | install/python.sh | All |
| Global npm | packages/npm.txt | install/npm.sh | All |
| Claude plugins | packages/claude-plugins.txt | install/claude.sh | All |
| Claude MCP servers | packages/claude-mcp.txt | install/claude.sh | All |
Adding a package — priority order
Choose the first layer that applies:
1. cargo — Rust crates
# packages/cargo.txt
ripgrep
fd-find
Re-run: bash ~/dotfiles/install/rust.sh
2. Homebrew — everything else
# packages/Brewfile
brew "tool-name"
# macOS-only (casks, GUI apps, macOS services)
if OS.mac?
cask "some-app"
end
Re-run: brew bundle --file=~/dotfiles/packages/Brewfile
3. pip — Python packages
# packages/pip.txt
requests
black
Re-run: bash ~/dotfiles/install/python.sh
4. npm — npm-specific tools
# packages/npm.txt
@scope/package-name
Re-run: bash ~/dotfiles/install/npm.sh
5. Custom install script
Look at an existing install/ script for patterns, follow them, and add an INSTALL_* flag to bootstrap.sh.
Why cargo over Homebrew for some tools
fd, sd, and zoxide are in cargo.txt instead of Brewfile because:
$CARGO_HOME/binis already under$LOCAL_PLAT/— PLAT isolation is automatic- Rust crates compile cleanly from source on any platform
- The Homebrew formula for these often just calls
cargo installanyway
Do not install the same tool in both places. PLAT paths win on PATH — the Homebrew copy would install but never be used.
Why Homebrew for Linux
Homebrew on Linux installs inside a manylinux_2_28 container (AlmaLinux 8, glibc 2.28) so the compiled binaries work on any Linux host since ~2018. Most packages pour as precompiled bottles — no compilation needed. Homebrew bundles its own glibc 2.35, so the binaries are self-contained regardless of the host’s glibc version.
The same Brewfile works on macOS and Linux. if OS.mac? blocks (casks, GUI apps) are silently skipped on Linux.
Updating all packages
# Homebrew (macOS)
brew bundle --file=~/dotfiles/packages/Brewfile
# Homebrew (Linux) — re-run in container
bash ~/dotfiles/install/linux-packages.sh
# Cargo tools
bash ~/dotfiles/install/rust.sh
# Python venv
bash ~/dotfiles/install/python.sh
# Claude plugins + MCP servers
bash ~/dotfiles/install/claude.sh
Day-to-day workflow
Add a package
See Package management for the priority order. Quick reference:
# Rust tool → packages/cargo.txt, then:
bash ~/dotfiles/install/rust.sh
# Homebrew formula/cask → packages/Brewfile, then:
brew bundle --file=~/dotfiles/packages/Brewfile
# Python package → packages/pip.txt, then:
bash ~/dotfiles/install/python.sh
Edit a dotfile
chezmoi edit ~/.zshrc # opens in $EDITOR, applies on save
chezmoi edit ~/.zprofile
chezmoi edit ~/.gitconfig
Or edit the source directly and apply:
$EDITOR ~/dotfiles/home/dot_zshrc.tmpl
chezmoi apply
Preview before applying: chezmoi diff
Sync dotfiles from the repo
chezmoi update # git pull + chezmoi apply
Update AI agent instructions
Claude (~/.claude/CLAUDE.md) and Codex (~/.codex/AGENTS.md) mirror each other — edit both:
chezmoi edit ~/.claude/CLAUDE.md
chezmoi edit ~/.codex/AGENTS.md
Add an env var or PATH entry
Edit home/dot_zprofile.tmpl. For anything arch-specific use $_LOCAL_PLAT (set at shell startup):
export MY_TOOL_HOME="$_LOCAL_PLAT/my-tool"
export PATH="$MY_TOOL_HOME/bin:$PATH"
Also add the variable to install/_lib.sh so install scripts can reference the same path.
Work on the docs
cd ~/dotfiles/docs && mdbook serve --open # live reload at localhost:3000
Every push to main auto-deploys to dotfiles.cade.io via Cloudflare Pages.
Deploy infrastructure changes
cd ~/dotfiles/infra/cloudflare
export CLOUDFLARE_API_TOKEN=...
tofu plan # preview
tofu apply # apply
terraform.tfvars is gitignored — it holds account_id and stays local.
Commit and push
cd ~/dotfiles
git add -p # stage selectively
git commit -m "description"
git push
Natural commit points: one commit per feature, config change, or coherent set of package additions.
Troubleshooting
Quick reference for when things go wrong. Check here before digging into scripts.
Tool not found after bootstrap
echo "$_PLAT" # confirm which platform the shell resolved
ls ~/.local/$_PLAT/bin/ # chezmoi, uv, claude should be here
ls ~/.local/$_PLAT/cargo/bin/ # fd, sd, zoxide, etc.
which fd # should point into ~/.local/$_PLAT/
If $_PLAT is empty or wrong, .zprofile wasn’t sourced. Open a new login shell (zsh -l) or source it:
source ~/.zprofile
nvm or node not available in a script
nvm.sh is lazy-loaded in interactive shells only. Non-interactive shells get node/npm via the PATH entry .zprofile adds from the highest installed version. If node is missing in a script, either:
# Option 1: source zprofile at the top of your script
source ~/.zprofile
# Option 2: use the full path
NODE="$NVM_DIR/versions/node/$(ls $NVM_DIR/versions/node | sort -V | tail -1)/bin/node"
Homebrew on Linux: Docker/Podman not found
linux-packages.sh requires a container runtime. Options:
# Rootless Docker
curl -fsSL https://get.docker.com/rootless | sh
# Podman (Debian/Ubuntu)
apt install podman
# Skip packages entirely and install manually
INSTALL_PACKAGES=0 ~/dotfiles/bootstrap.sh
See Bootstrap → Linux for full setup instructions.
chezmoi keeps prompting for name/email
The cached values live in ~/.config/chezmoi/chezmoi.toml. To reset:
chezmoi init --data=false
To pre-seed without prompting:
CHEZMOI_NAME="Your Name" CHEZMOI_EMAIL="you@example.com" chezmoi init
chezmoi diff shows unexpected changes
Another program modified a managed file (e.g. Claude Code updated ~/.claude/settings.json). Options:
chezmoi diff # see what changed
chezmoi apply # overwrite with repo version
chezmoi add ~/.claude/settings.json # pull the live version into the repo
PATH order is wrong — wrong binary is resolving
Expected priority (highest to lowest):
~/.local/$PLAT/venv/bin Python venv
~/.local/$PLAT/cargo/bin Rust tools (fd, sd, zoxide, etc.)
~/.local/$PLAT/nvm/.../bin Node.js
~/.local/$PLAT/bin chezmoi, uv, claude (Linux)
~/.local/bin arch-neutral scripts
/opt/homebrew/bin Homebrew (macOS)
/usr/bin system
Diagnose with:
which <tool> # where it's resolving from
type -a <tool> # all locations on PATH
echo $PATH | tr ':' '\n' # full PATH in order
If a Homebrew tool is shadowing a cargo tool, check packages/cargo.txt and packages/Brewfile for duplicates — remove the one you don’t want.
Cloudflare Pages build failing
Check the build log via the API:
ACCOUNT="5afb385ba43e1a082b138554dfdb141c"
TOKEN="..."
# List recent deployments
curl -s "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT/pages/projects/dotfiles/deployments" \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool | grep -E '"id"|"status"'
# Get logs for a specific deployment
DEPLOY_ID="..."
curl -s "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT/pages/projects/dotfiles/deployments/$DEPLOY_ID/history/logs" \
-H "Authorization: Bearer $TOKEN" | python3 -c "
import sys, json
for e in json.load(sys.stdin)['result']['data']: print(e['line'])
"
Common causes:
cargo-binstall: command not found—/opt/buildhome/.cargo/binnot on PATH; checkinfra/cloudflare/build.shmdbook: command not found— binstall failed; check network or fall back tocargo install mdbook --locked- Build output not found — confirm
destination_dir = "docs/book"ininfra/cloudflare/main.tf
Two machines fighting over dotfiles on a shared home
This happens when a template renders differently on each machine (e.g. using {{ .chezmoi.arch }}). The rule: templates must be arch-neutral. Arch-specific logic belongs in shell runtime code, not templates.
Check which template is causing the conflict:
chezmoi diff # shows what chezmoi wants to change vs what's on disk
The fix is almost always to replace a template variable with a shell runtime expression. See Managing dotfiles → Shared home safety.