cade’s dotfiles
Personal dotfiles for macOS and Linux. One command bootstraps a complete dev environment — idempotent, no sudo on Linux, and (optionally) safe on shared NFS home directories across CPU architectures.
DF_NAME="Your Name" DF_EMAIL="[email protected]" \
curl -fsSL https://raw.githubusercontent.com/cadebrown/dotfiles/main/bootstrap.sh | bash
DF_NAME / DF_EMAIL are needed when piping into bash (the pipe occupies stdin, so chezmoi can’t prompt); from a local clone, ~/dotfiles/bootstrap.sh prompts interactively. Re-run anytime to converge.
Pick your path:
| Goal | Page |
|---|---|
| Set up a brand-new machine | Bootstrap |
| Sync the latest changes | Day-to-day workflow |
| Add or remove a tool | Package management |
| Understand PLAT isolation | PLAT isolation |
| Set up API tokens | Auth |
| Create a private extension | Overlays |
Look up a DF_* flag | Env-var reference |
Trace what bootstrap.sh actually does | Bootstrap flow |
What gets installed
Dotfiles and shell
chezmoi manages dotfiles as templates in home/ and applies them to ~/. Both zsh and bash get identical login profiles with PLAT detection, PATH setup, and tool activation.
- zsh: oh-my-zsh with pure prompt, autosuggestions, fast-syntax-highlighting, completions, and lazy nvm loading (~140ms startup)
- bash: minimal config with git branch prompt, shared aliases, zoxide, fzf completions
- git: global config with name/email from chezmoi data, delta as pager
- SSH: templated config from
home/dot_ssh/config.tmpl
Packages
A single packages/Brewfile drives both platforms. On macOS, Homebrew installs native bottles plus casks (GUI apps). On Linux, Homebrew installs to a custom prefix ($_LOCAL_PLAT/brew/) with its own glibc — fully self-contained, no sudo.
if OS.mac? blocks in the Brewfile handle macOS-only casks and tools; Linux skips them silently.
Languages
| Language | Tool | Install location | Package list |
|---|---|---|---|
| Rust | rustup + cargo-binstall | $LOCAL_PLAT/rustup/, $LOCAL_PLAT/cargo/ | packages/cargo.txt |
| Node.js | nvm (lazy-loaded in zsh) | $LOCAL_PLAT/nvm/ | packages/npm.txt |
| Python | uv tool install (per CLI tool) | $LOCAL_PLAT/uv/tools/, entrypoints in $LOCAL_PLAT/bin/ | packages/pip.txt |
Rust tools install via cargo-binstall (downloads pre-built binaries from GitHub releases when available, falls back to source). Python CLI tools each get their own isolated venv via uv tool install — no monolithic user-level environment. On macOS, rustup comes from Homebrew (code-signed, required on Sequoia+ where the linker enforces provenance).
AI tools
- Claude Code — native binary from Anthropic’s release bucket, plus plugins (
packages/claude-plugins.txt) and MCP servers (packages/mcp-servers.txt) - Codex CLI — npm-installed binary (
@openai/codexinnpm.txt), with managed config + hooks underhome/dot_codex/and[mcp_servers.*]blocks generated from the sharedpackages/mcp-servers.txt - Cursor / VS Code — extension lists in
packages/{cursor,vscode}-extensions.txt; Cursor settings symlinked fromhome/dot_cursor/
macOS-specific
- System settings (
install/macos-settings.sh) — Dock autohide, Finder extensions/path bar, fast key repeat, tap to click, PNG screenshots, Safari dev menu, iTerm2 prefs - Services (
install/macos-services.sh) — Colima registered as a login service (rootless Docker without Docker Desktop) - Quick Actions (
install/macos-quick-actions.sh) — Finder right-click “Open in Cursor” and friends
Auth (opt-in)
install/auth.sh is a guided service-registry helper that creates ~/.<service>.env files (chmod 600) for GitHub, Anthropic, OpenAI, Cloudflare, and HuggingFace — plus a separate gh auth login flow for the Claude GitHub MCP. Sourced automatically by all install scripts and login shells. Run during bootstrap with DF_DO_AUTH=1 or standalone anytime.
Home directories
install/dirs.sh creates ~/dev, ~/bones, and ~/misc (configurable via DF_DIRS). On systems with scratch space, these become symlinks directly under $SCRATCH/ for fast local storage. See Scratch space.
PLAT isolation (optional)
By default $LOCAL_PLAT = $HOME/.local and everything lives under a flat ~/.local/. PLAT isolation is opt-in — set DF_USE_PLAT=1 (or use_plat = true in chezmoi data) and $LOCAL_PLAT becomes ~/.local/$PLAT/. The point: on a shared NFS home, each machine installs into its own PLAT directory; one home directory, many machines, no conflicts. Single-machine users get the simpler flat layout without the per-PLAT directory tax.
DF_USE_PLAT=0 (default, flat) DF_USE_PLAT=1 (NFS-shared homes)
───────────────────────────── ───────────────────────────────────
~/.local/ ~/.local/
├── bin/ ├── plat_Darwin_arm64/
│ ├── chezmoi │ ├── bin/{chezmoi,uv,claude}
│ ├── uv │ ├── brew/ (Apple Silicon)
│ └── claude │ ├── cargo/bin/ (arm64 binaries)
├── brew/ (one prefix) │ └── nvm/ (arm64 node)
├── cargo/bin/ (host arch) ├── plat_Linux_x86-64-v3/
└── nvm/ │ ├── brew/ (AVX2 glibc)
│ └── ...
$_LOCAL_PLAT = ~/.local └── plat_Linux_x86-64-v4/ (AVX-512)
└── ...
$_LOCAL_PLAT = ~/.local/$_PLAT
(set per-shell from CPU detection)
Capability detection still runs in flat mode — .plat_env.sh tunes compiler flags (-march=x86-64-v3, RUSTFLAGS=-Ctarget-cpu=apple-m1, etc.) for the host CPU even when directory isolation is off. See PLAT isolation for the decision matrix.
macOS vs Linux
| macOS | Linux | |
|---|---|---|
| Packages | Homebrew at /opt/homebrew | Homebrew at ~/.local/$PLAT/brew/ (custom prefix, bundled glibc) |
| Rust | Homebrew rustup (code-signed for Sequoia) | sh.rustup.rs |
| System settings | Dock, Finder, keyboard, trackpad, Safari, iTerm2 | – |
| Services | Colima (rootless Docker) | – |
| sudo required | Yes (Homebrew installer) | No |
Bootstrap modes
bootstrap.sh # install (default) — full idempotent setup
bootstrap.sh update # git pull + chezmoi apply + refresh tools
bootstrap.sh upgrade # update + brew upgrade + cargo upgrade
Any step can be skipped with DF_DO_*=0 env vars. See Bootstrap for the full list.
Sections
Setup
| Page | What it covers |
|---|---|
| Bootstrap | System requirements, what gets installed, skip flags, modes |
| Managing dotfiles | chezmoi workflow, editing dotfiles, template variables, shared-home safety |
| Package management | Adding tools via cargo, npm, pip, or Homebrew |
| PLAT isolation | When to use it, layouts compared, decommissioning |
| Auth | Service registry, env-file flow, gh-derive trick |
| Scratch space | Symlink topology for NFS-quota relief |
| Overlays | Private extension repos (dotfiles-*/) |
Usage
| Page | What it covers |
|---|---|
| Day-to-day workflow | Updating, adding packages, editing dotfiles |
| AeroSpace window management | Tiling WM keymap (macOS) |
| Local AI coding | Ollama, mlx-lm, opencode, pi setup |
| Troubleshooting | Tools not found, PATH issues, build failures |
Reference
| Page | What it covers |
|---|---|
Env vars (DF_*) | Complete table of every flag and behavior var |
| Bootstrap flow | Step-by-step diagram of what bootstrap.sh does |
Infrastructure
| Page | What it covers |
|---|---|
| Docs and hosting | How this site is built, deployed, and managed |
Bootstrap a new machine
One-liner
DF_NAME="Your Name" DF_EMAIL="[email protected]" \
curl -fsSL https://raw.githubusercontent.com/cadebrown/dotfiles/main/bootstrap.sh | bash
Runs fully unattended. DF_NAME / DF_EMAIL are needed here because piping the
script into bash occupies stdin, leaving chezmoi no terminal to prompt on. The
values are cached in ~/.config/chezmoi/chezmoi.toml, so re-runs read from the
cache and need nothing.
Interactive (prompts for name + email)
To be prompted instead of pre-seeding, run from a local clone in a real terminal — chezmoi then has a TTY to read from:
git clone https://github.com/cadebrown/dotfiles ~/dotfiles
~/dotfiles/bootstrap.sh
Modes
bootstrap.sh # install (default) — full idempotent setup
bootstrap.sh update # git pull + chezmoi apply + refresh tools
bootstrap.sh upgrade # update + brew upgrade + cargo upgrade
update pulls the latest dotfiles, applies chezmoi, refreshes zsh plugins, and re-runs all install scripts (which skip already-installed tools). Skips scratch setup and repo cloning.
upgrade does everything update does, plus enables Homebrew upgrades (DF_BREW_UPGRADE=1) and forces cargo-binstall to re-check for newer binaries.
macOS
Requirements
| Requirement | How to get it |
|---|---|
| macOS 13+ (Ventura or later) | — |
| Xcode Command Line Tools | Homebrew prompts automatically, or: xcode-select --install |
| Internet access | — |
Sudo is required for the Homebrew installer.
What gets installed
Paths below use $LOCAL_PLAT, which is $HOME/.local by default and $HOME/.local/$PLAT when PLAT isolation is enabled. $ARCH_BIN is $LOCAL_PLAT/bin.
- chezmoi →
$ARCH_BIN/chezmoi - Dotfiles applied via
chezmoi apply- Shell configs for both zsh (
.zprofile) and bash (.bash_profile) - Both shells do identical PLAT capability detection and PATH setup
- Shell configs for both zsh (
- oh-my-zsh + plugins (pure prompt, autosuggestions, fast-syntax-highlighting, completions)
- Homebrew →
/opt/homebrew(Apple Silicon) or/usr/local(Intel)- All packages from
packages/Brewfile— CLI tools, casks, macOS-only apps - Includes
rustup(Homebrew’s code-signed build — required for macOS Sequoia+)
- All packages from
- Services: colima registered as a login service (rootless Docker)
- macOS defaults: Dock, Finder, keyboard, trackpad, screenshots, Safari, iTerm2 preferences
- Node.js via nvm →
$LOCAL_PLAT/nvm/ - Rust toolchain →
$LOCAL_PLAT/rustup/+$LOCAL_PLAT/cargo/- Homebrew’s
rustup(code-signed), required on macOS Sequoia+ where the linker enforcescom.apple.provenance cargo-binstalldownloads pre-built binaries from GitHub releases when available, falls back to source- Cargo tools install to
$LOCAL_PLAT/cargo/bin/
- Homebrew’s
- Python via uv →
$LOCAL_PLAT/uv/tools/<tool>/(one isolated venv per CLI tool), entrypoints in$ARCH_BIN - Claude Code native binary →
$ARCH_BIN/claude+ plugins + MCP servers + overlay skills - Codex CLI native binary →
$ARCH_BIN/codex, plus managed config + hooks under~/.codex/ - Cursor / VS Code — settings symlinked from
home/dot_cursor/; extensions installed frompackages/{cursor,vscode}-extensions.txt - CMake toolchain files →
$LOCAL_PLAT/cmake/toolchains/- Versioned files:
llvm-21.cmake,llvm-22.cmake,gcc-13.cmake,gcc-15.cmake, plus shared_brew.cmake ~/.profilesetsCMAKE_TOOLCHAIN_FILEto the highest installed LLVM toolchain automatically- Switch at runtime with the
tcshell function (e.g.tc gcc-15,tc llvm-22)
- Versioned files:
- Local LLM tooling — HuggingFace cache + binary checks
- Creates
$LOCAL_PLAT/.cache/huggingfacefor mlx-lm weights - Verifies ollama / mlx-lm / mlx-openai-server / opencode binaries
- Creates
- Agent memory stack — cass session-history search, ~/kb + qmd knowledge index, memory daemons
- Blender MCP addon — installs
addon.pyinto the active Blender profile and enables it - Auth (opt-in:
DF_DO_AUTH=1) — guided service-token setup; see Auth - Overlays — runs
bootstrap.shof anydotfiles-*/overlay alongside this repo; see Overlays
Total time: ~2 minutes on subsequent runs (idempotent, mostly bottle pours); ~5–10 minutes on a fresh machine.
Linux
Requirements
| Requirement | Notes |
|---|---|
| x86_64 or aarch64 | — |
git and curl | Pre-installed on most systems |
| Internet access | — |
No sudo required. No Docker or Podman needed.
What gets installed
Paths use $LOCAL_PLAT, which is $HOME/.local by default (or $HOME/.local/$PLAT with PLAT isolation enabled — recommended for shared NFS homes).
- chezmoi →
$ARCH_BIN/chezmoi($ARCH_BIN=$LOCAL_PLAT/bin) - Dotfiles applied via
chezmoi apply- Shell configs for both zsh (
.zprofile) and bash (.bash_profile)
- Shell configs for both zsh (
- oh-my-zsh + plugins
- Homebrew →
$LOCAL_PLAT/brew/(native install, no Docker/Podman needed)- Installs Homebrew’s own glibc 2.35 first — binaries are fully self-contained
- Most packages pour as precompiled bottles; glibc builds from source (~2 min) on first run
- Custom [email protected] patches applied automatically for Linux compatibility
- Node.js via nvm →
$LOCAL_PLAT/nvm/ - Rust via
sh.rustup.rs→$LOCAL_PLAT/rustup/+$LOCAL_PLAT/cargo/cargo-binstalldownloads pre-built binaries from GitHub releases when available, falls back to source
- Python via uv →
$LOCAL_PLAT/uv/tools/<tool>/(per-CLI-tool venvs), entrypoints in$ARCH_BIN - Claude Code native binary →
$ARCH_BIN/claude+ plugins + MCP servers - Codex CLI native binary →
$ARCH_BIN/codex - Cursor / VS Code — extensions from
packages/{cursor,vscode}-extensions.txt - CMake toolchain files →
$LOCAL_PLAT/cmake/toolchains/(llvm-21/22.cmake,gcc-13/15.cmake,_brew.cmake)~/.profileauto-setsCMAKE_TOOLCHAIN_FILEto the highest installed LLVM toolchain- Switch with the
tcshell function (e.g.tc gcc-15,tc llvm-22)
- Local LLM tooling — HuggingFace cache + ollama/mlx-lm/mlx-openai-server/opencode binary checks
- Agent memory stack — cass session-history search, ~/kb + qmd knowledge index (daemons lazy-start from shell profiles)
- Auth (opt-in:
DF_DO_AUTH=1) — guided token setup; see Auth - Overlays — runs
bootstrap.shof anydotfiles-*/overlay; see Overlays
Total time: ~5 minutes on a fast connection.
Skipping steps
Any step can be disabled with an environment variable:
DF_DO_SCRATCH=0 # skip scratch space symlink setup
DF_DO_DIRS=0 # skip home directory creation (~/dev, ~/bones, ~/misc)
DF_DO_PACKAGES=0 # skip Homebrew + brew bundle
DF_DO_MACOS_SERVICES=0 # skip colima service setup (macOS)
DF_DO_MACOS_SETTINGS=0 # skip macOS settings (Dock, Finder, keyboard, etc.)
DF_DO_MACOS_QUICK_ACTIONS=0 # skip Finder Quick Actions install (macOS)
DF_DO_ZSH=0 # skip oh-my-zsh
DF_DO_NODE=0 # skip nvm + Node.js + global npm packages
DF_DO_RUST=0 # skip rustup + cargo tools
DF_DO_PYTHON=0 # skip uv + per-tool venvs
DF_DO_CLAUDE=0 # skip Claude Code install + plugins + MCP servers
DF_DO_CODEX=0 # skip Codex CLI install
DF_DO_CURSOR=0 # skip Cursor settings symlinks + extension install
DF_DO_VSCODE=0 # skip VS Code extension install
DF_DO_CMAKE=0 # skip CMake toolchain file deployment
DF_DO_LOCAL_LLM=0 # skip local LLM setup (HuggingFace cache + binary checks)
DF_DO_MEMORY=0 # skip the agent memory stack (cass + qmd + ~/kb)
DF_DO_BLENDER_MCP=0 # skip Blender MCP addon install
DF_DO_AUTH=1 # run interactive API token setup (default 0)
DF_DO_OVERLAYS=0 # skip all overlay bootstraps (dotfiles-*/bootstrap.sh)
DF_USE_PLAT=1 # opt in to per-PLAT directory isolation (default 0; flat layout)
DF_BREW_UPGRADE=0 # skip Homebrew upgrades (macOS default: 1, Linux default: 0)
The complete reference lives at Env vars.
Example — dotfiles only, no runtimes:
DF_DO_PACKAGES=0 DF_DO_ZSH=0 DF_DO_NODE=0 \
DF_DO_RUST=0 DF_DO_PYTHON=0 DF_DO_CLAUDE=0 \
~/dotfiles/bootstrap.sh
Debug mode
For verbose output with command timing:
DF_DEBUG=1 ~/dotfiles/bootstrap.sh
Shows [dbug] lines for every command executed by run_logged, including exit codes and elapsed time.
Shared home directories (NFS/GPFS)
If you share $HOME across multiple machines with different CPU architectures, enable PLAT isolation:
DF_USE_PLAT=1 ~/dotfiles/bootstrap.sh
(Or persist it in chezmoi data: chezmoi edit ~/.config/chezmoi/chezmoi.toml and set use_plat = true.)
With PLAT on, each machine installs compiled tools to its own ~/.local/$PLAT/ directory:
| Machine | PLAT | Where tools live |
|---|---|---|
| AVX-512 Linux (e.g. Ice Lake) | plat_Linux_x86-64-v4 | ~/.local/plat_Linux_x86-64-v4/ |
| AVX2 Linux (e.g. Haswell/Zen2) | plat_Linux_x86-64-v3 | ~/.local/plat_Linux_x86-64-v3/ |
| ARM Linux | plat_Linux_aarch64 | ~/.local/plat_Linux_aarch64/ |
| Apple Silicon | plat_Darwin_arm64 | ~/.local/plat_Darwin_arm64/ |
Text configs (dotfiles) are arch-neutral and shared freely across all machines. See PLAT isolation for the deeper explanation, the decommission script, and the failure modes that PLAT exists to prevent.
Scratch space (large quota environments)
If your home directory has a small quota (common on HPC NFS mounts), direct large directories to local scratch storage:
DF_SCRATCH=/scratch/$USER \
DF_NAME="Your Name" DF_EMAIL="[email protected]" \
~/dotfiles/bootstrap.sh
This symlinks large directories to $DF_SCRATCH/.paths/ before any tools are installed, so the multi-GB Homebrew prefix and caches never touch NFS.
Default directories redirected to scratch (controlled by DF_LINKS):
~/.local— PLAT directories, Homebrew prefix, tool binaries~/.cache— ccache, sccache, pip/uv cache~/.vscode/~/.vscode-server— VS Code extensions and data~/.cursor/~/.cursor-server— Cursor IDE data~/.nv— NVIDIA shader and OptiX cache~/.npm— npm cache~/.claude— Claude Code data and cache~/.oh-my-zsh/~/.oh-my-zsh-custom— oh-my-zsh and plugins
Auth (API tokens)
See the dedicated Auth page for the full walkthrough. Quick reference:
bash ~/dotfiles/install/auth.sh # walk every service interactively
bash ~/dotfiles/install/auth.sh status # show current state, no prompts
bash ~/dotfiles/install/auth.sh huggingface # set/update one service
bash ~/dotfiles/install/auth.sh gh # `gh auth login` (browser flow)
# Or during bootstrap:
DF_DO_AUTH=1 ~/dotfiles/bootstrap.sh
Covers GitHub, Anthropic, OpenAI, Cloudflare, HuggingFace, plus a separate gh auth login flow for the Claude GitHub MCP. Tokens land in ~/.<service>.env files (chmod 600) and are auto-sourced by install scripts and login shells. Each prompt shows a skip if: hint — most users only set 1–2 of them.
Managing dotfiles
chezmoi manages the files in home/ and applies them to ~/, resolving templates along the way.
Data flow
sequenceDiagram
participant U as User
participant B as bootstrap.sh
participant CZ as chezmoi
participant T as ~/.config/chezmoi/<br/>chezmoi.toml
participant S as home/dot_X.tmpl<br/>(repo source)
participant H as ~/.X<br/>(target)
U->>B: run bootstrap.sh
B->>CZ: chezmoi init (first run only)
CZ->>U: prompt name + email (needs a TTY; skipped if DF_NAME / DF_EMAIL pre-set)
U-->>CZ: "Cade", "brown.cade@..."
CZ->>T: cache values
B->>CZ: chezmoi apply
CZ->>T: read .name, .email, .use_plat
CZ->>S: read template
Note over CZ: render Go template — {{ .name }} expands,<br/>{{ if eq .chezmoi.os "linux" }} branches, etc.
CZ->>H: write rendered file (overwrites!)
Note over H: never edit ~/.X directly —<br/>next apply overwrites it
Templates render at apply time using the values in ~/.config/chezmoi/chezmoi.toml. The prompt only fires if a value is missing — re-runs read from cache.
The quick version
chezmoi edit ~/.zshrc # edit a dotfile (opens in $EDITOR, applies on save)
chezmoi edit ~/.zprofile # zsh login shell config
chezmoi edit ~/.bash_profile # bash login shell config (mirrors .zprofile)
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_zprofile.tmpl | ~/.zprofile (zsh login shell) |
home/dot_bash_profile.tmpl | ~/.bash_profile (bash login shell) |
home/dot_config/git/ignore | ~/.config/git/ignore |
home/dot_ssh/config.tmpl | ~/.ssh/config |
home/dot_claude/CLAUDE.md | ~/.claude/CLAUDE.md |
home/dot_codex/AGENTS.md | ~/.codex/AGENTS.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)
{{ .use_plat }} PLAT directory isolation flag (default false; see PLAT page)
{{ .chezmoi.os }} "darwin" or "linux"
{{ .chezmoi.arch }} "amd64" or "arm64" ← do NOT use in shared-NFS templates
{{ .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 # zsh login shell
chezmoi edit ~/.bash_profile # bash login shell
Directly in the repo (then apply manually):
$EDITOR ~/dotfiles/home/dot_zshrc.tmpl
$EDITOR ~/dotfiles/home/dot_zprofile.tmpl
$EDITOR ~/dotfiles/home/dot_bash_profile.tmpl
chezmoi apply
Never edit ~/.zshrc, ~/.zprofile, or ~/.bash_profile directly — chezmoi will overwrite them 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.
Multi-machine sync
chezmoi apply only affects the machine it runs on. Each home is independent — macOS
(/Users/cadeb/) and Linux NFS (/home/cadeb/) don’t share target files.
Normal workflow — commit first, then sync remotes:
# 1. Edit and apply locally
chezmoi edit ~/.ssh/config
chezmoi apply
# 2. Commit and push
cd ~/dotfiles
git add home/dot_ssh/config.tmpl
git commit -m "ssh: describe what changed"
git push
# 3. On each remote — pull and apply
ssh remote-host 'bash -l ~/dotfiles/bootstrap.sh update'
If you applied locally without committing (the wrong order), remotes are stale. Quick workaround while you clean it up:
# Render the template locally and copy the result over
chezmoi cat ~/.ssh/config | ssh remote-host 'cat > ~/.ssh/config'
Then commit and push so the repo catches up.
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
Codex-specific note:
~/.codex/AGENTS.mdand~/.codex/rules/are intentionally Codex-specific; skills are shared from~/.claude/skillsvia the~/.agents/skillssymlink
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 (native, no 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/node.sh | All |
| Go CLI tools | packages/go.txt | install/go.sh | All (respects # linux-only / # macos-only) |
| Claude plugins | packages/claude-plugins.txt | install/claude.sh | All |
| MCP servers (Claude + Codex) | packages/mcp-servers.txt | install/claude.sh, install/codex.sh | All |
| Codex CLI/config | home/dot_codex/ | install/codex.sh | All |
| Cursor extensions | packages/cursor-extensions.txt | install/cursor.sh | All |
| VS Code extensions | packages/vscode-extensions.txt | install/vscode.sh | All |
Adding a package — priority order
Choose the first layer that applies. Native installers first, Homebrew as fallback:
1. cargo — Rust crates
# Add to packages/cargo.txt
fd-find
ripgrep
bat
typst-cli
my-new-tool
Re-run: bash ~/dotfiles/install/rust.sh
install/rust.sh uses cargo-binstall: it tries to
download a pre-built binary from GitHub releases first (fast, no compilation), and falls back to
cargo install (source compilation) if no binary is available.
On Linux, cargo-binstall avoids the manylinux container round-trip entirely. On macOS, it downloads the same pre-built binary that Homebrew bottles provide — same quality, faster install.
macOS note: Source compilation requires running from a normal terminal. The macOS Sequoia linker enforces
com.apple.provenanceon object files and will block compilation in sandboxed contexts (e.g., certain CI environments). This isn’t an issue for day-to-day use.
2. npm — npm-specific tools
# packages/npm.txt
@earendil-works/pi-coding-agent
Re-run: bash ~/dotfiles/install/node.sh
Currently ships pi — a multi-provider coding agent (Claude / OpenAI / Gemini / etc.). The official pi.dev/install.sh ultimately runs npm install -g @earendil-works/pi-coding-agent, so we list it here directly.
Other CLI agents are installed via their native packagers:
claude-code→install/claude.sh(Anthropic GCS binary)codex→@openai/codex(version-pinned) inpackages/npm.txt; managed config viainstall/codex.shopencode→brew "opencode"(packages/Brewfile)
Codex CLI config, rules, themes, and MCP servers are managed from home/dot_codex/
(skills live in home/dot_claude/skills/, shared via the ~/.agents/skills symlink).
install/codex.sh sync-config preserves runtime trust/plugin sections while refreshing the
managed config. Chezmoi also runs this sync when home/dot_codex/create_config.toml changes.
3. pip — Python packages
# packages/pip.txt
requests
black
numpy
some-macos-tool # macos-only (requires Metal / only available on macOS)
Re-run: bash ~/dotfiles/install/python.sh
Each tool gets its own isolated venv via uv tool install, with entrypoints in $LOCAL_PLAT/bin/.
Comment conventions parsed by install/python.sh:
# macos-only— skipped on Linux (e.g.mlx-lmrequires Apple Metal/MLX framework)# python=X.Y— pins to a specific Python version for that tool (e.g.mlx-openai-serverneeds 3.12 becauseoutlines-corehas no cp313/cp314 wheels)
4. Homebrew — non-language-specific tools and C libraries
# packages/Brewfile
brew "tool-name"
# macOS-only (casks, GUI apps, macOS-specific services)
if OS.mac?
cask "some-app"
brew "macos-only-tool"
end
Re-run: brew bundle --file=~/dotfiles/packages/Brewfile
if OS.mac? blocks are silently skipped on Linux. Everything outside those blocks runs on both platforms.
Prefer Homebrew for tools that aren’t available via cargo/npm/pip, have complex C dependencies, or are macOS-specific (casks, GUI apps).
5. VS Code / Cursor extensions
Both editors have separate extension lists since marketplace availability differs (Cursor uses OpenVSX, which doesn’t carry every Microsoft-restricted extension).
# packages/vscode-extensions.txt (VS Code marketplace)
# packages/cursor-extensions.txt (OpenVSX, Cursor)
ms-python.python
charliermarsh.ruff
myriad-dreamin.tinymist # Typst LSP — works in both
Re-run: bash ~/dotfiles/install/vscode.sh and/or bash ~/dotfiles/install/cursor.sh
To capture newly installed extensions back into the file (union — never removes):
bash ~/dotfiles/install/vscode.sh sync-extensions
bash ~/dotfiles/install/cursor.sh sync-extensions
Note: VS Code
settings.jsonis not tracked (contains embedded credentials in some setups). Cursor’s settings ARE tracked via symlinks underhome/dot_cursor/.
6. Custom install script
Look at an existing install/ script for patterns and follow them. Add a DF_DO_* flag to bootstrap.sh.
Local AI tools
Local LLM inference and coding agents are split across three layers:
| Tool | Layer | Notes |
|---|---|---|
ollama | packages/Brewfile (macOS only) | Inference server; installed as Homebrew formula, managed as a LaunchAgent |
opencode | packages/Brewfile | TUI coding agent by the SST team |
mlx-lm | packages/pip.txt | Apple Silicon Metal inference; on-demand only |
just | packages/cargo.txt | Command runner / Makefile alternative |
install/local-llm.sh creates the PLAT-isolated HuggingFace cache directory ($LOCAL_PLAT/.cache/huggingface)
and verifies that the expected binaries are present. install/opencode.sh verifies the opencode binary; opencode’s backend config is pure chezmoi (opencode.json.tmpl, MLX primary).
See Local AI coding for usage details.
Don’t duplicate across layers
Do not install the same tool in both cargo.txt and Brewfile. PLAT paths (~/.local/$PLAT/) come first on PATH — the Homebrew copy would install but never be used. If a tool is in cargo.txt, it must not be in Brewfile, and vice versa.
Why cargo over Homebrew for Rust tools
Tools like fd, sd, bat, ripgrep, git-delta, difftastic, procs, bottom,
ast-grep, zoxide, and hyperfine live in cargo.txt because:
$CARGO_HOME/bin/is already under$LOCAL_PLAT/— PLAT isolation is freecargo-binstalldownloads pre-built GitHub release binaries — fast, no compilation
Tools that have no pre-built binary and are painful to compile (or only make sense on macOS) go in
Brewfile under if OS.mac?.
Why Homebrew for Linux
Homebrew on Linux installs natively on the host (no container, no sudo). It bundles its own glibc 2.35, making binaries fully self-contained regardless of the host’s glibc version.
Custom prefix tradeoff: Installing to ~/.local/$PLAT/brew/ instead of the standard
/home/linuxbrew/.linuxbrew enables per-CPU isolation on shared NFS homes, but bottles
built for the standard prefix can’t always be relocated:
- Relocatable packages (jq, CLI tools with simple dependencies) pour as bottles — patchelf rewrites RPATH and they work fine
- Deep path embedding (Python, Perl, git, vim, ffmpeg, imagemagick) build from source
on first install. Homebrew uses all available CPU cores (auto-detects
nproc), so builds are fast on modern hardware.
Once built, packages are cached. Subsequent runs and upgrades are bottle-only.
Compilers: gcc and llvm are keg-only (Homebrew doesn’t create unversioned gcc/clang
symlinks to avoid shadowing system compilers). linux-packages.sh creates symlinks in
$LOCAL_PLAT/bin/ so gcc → the highest installed GCC and clang → llvm@21/bin/clang.
See Compiler toolchains below for CMake integration.
[email protected] patches: On Linux, install/patch-homebrew-python.sh automatically patches
the [email protected] formula to fix build issues (uuid module detection, test_datetime PGO hangs).
Patches are applied during bootstrap and protected by HOMEBREW_NO_AUTO_UPDATE=1.
The same Brewfile works on macOS and Linux. if OS.mac? blocks are silently skipped on Linux.
Compiler toolchains
CMake compiler selection is handled by toolchain files deployed per-PLAT, not by raw
CC/CXX env vars. install/cmake.sh copies them from install/cmake/toolchains/
to $LOCAL_PLAT/cmake/toolchains/ on every bootstrap run (always overwrites, so they
stay in sync with the repo).
Default: LLVM (Homebrew clang)
Toolchain files are versioned: llvm-21.cmake, llvm-22.cmake, gcc-13.cmake, gcc-15.cmake, plus a shared _brew.cmake helper.
When Homebrew LLVM is present, ~/.profile auto-sets:
export CMAKE_TOOLCHAIN_FILE="$_LOCAL_PLAT/cmake/toolchains/llvm-22.cmake"
# (highest installed LLVM version wins; falls back to llvm-21)
The toolchain configures:
| CMake variable | Value |
|---|---|
CMAKE_C_COMPILER | $_LOCAL_PLAT/brew/opt/llvm@22/bin/clang (or unversioned opt/llvm/) |
CMAKE_CXX_COMPILER | $_LOCAL_PLAT/brew/opt/llvm@22/bin/clang++ |
CMAKE_AR / CMAKE_RANLIB | llvm-ar, llvm-ranlib (LTO needs the matching tool) |
CMAKE_LINKER_TYPE | MOLD > LLD (Linux only; macOS uses Apple’s ld) |
CMAKE_CUDA_COMPILER | $_LOCAL_PLAT/.cuda/bin/nvcc (only if symlink set up) |
CMAKE_CUDA_HOST_COMPILER | clang++ (when CUDA available) |
CMake auto-detects nm/objcopy/objdump/strip from CC, so the toolchain files only override what actually matters.
Switching toolchains
Per-invocation:
CMAKE_TOOLCHAIN_FILE="$_LOCAL_PLAT/cmake/toolchains/gcc-15.cmake" cmake -B build
Per-session via the tc shell function:
tc # show active
tc list # list available
tc gcc-15 # GCC 15
tc gcc-13 # GCC 13
tc llvm-22 # LLVM 22
tc llvm-21 # LLVM 21
Per-project (CMakePresets.json):
{ "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "/absolute/path/to/gcc-15.cmake" } }
The GCC toolchains use versioned binaries (gcc-15, g++-15, etc.) because Homebrew doesn’t create unversioned gcc symlinks on macOS. Linux gets unversioned symlinks via linux-packages.sh, but the versioned files work on both. Linker priority on Linux: mold → lld → gold → system ld.
Disabling the toolchain
unset CMAKE_TOOLCHAIN_FILE # let CMake auto-detect compilers
CUDA
CUDA is not managed by bootstrap — install the toolkit separately (system package, NVIDIA runfile, or a module system on HPC). Then point the per-PLAT symlink at it:
ln -sfn /usr/local/cuda "$_LOCAL_PLAT/.cuda" # system default
ln -sfn /opt/nvidia/cuda/12.6 "$_LOCAL_PLAT/.cuda" # versioned install
~/.profile resolves the symlink at login and exports:
CUDA_PATHandCUDAToolkit_ROOT— picked up by CMake’sfind_package(CUDAToolkit)and most other build systems- Prepends
$CUDA_PATH/bintoPATHsonvccis on the path
Both toolchain files also set CMAKE_CUDA_COMPILER to $LOCAL_PLAT/.cuda/bin/nvcc when the
symlink exists, so enable_language(CUDA) works without any project-level configuration.
Different machines on a shared NFS home can point their $LOCAL_PLAT/.cuda symlinks at
different toolkit versions — no conflicts.
Switching toolchains at runtime
The tc shell function (defined in .zshrc) switches the active toolchain for the current session:
tc # show active toolchain
tc list # list available toolchain files
tc gcc-15 # switch to GCC 15 (sets CC/CXX/AR/RANLIB/NM + CMAKE_TOOLCHAIN_FILE)
tc gcc-13 # switch to GCC 13
tc llvm-22 # switch to LLVM 22 (clears CC/CXX; CMake file owns compiler selection)
tc llvm-21 # switch to LLVM 21
Compiler caching (ccache / sccache)
~/.profile configures ccache and sccache automatically when they’re installed:
| Setting | Value | Why |
|---|---|---|
CCACHE_BASEDIR | scratch root or $HOME | Rewrites absolute paths to relative before hashing — builds in different directories share cache hits |
CCACHE_COMPILERCHECK | content | Hash compiler by content, not mtime — survives brew reinstalls and module swaps |
CCACHE_SLOPPINESS | file_stat_matches,time_macros | Use mtime+size for include checks; cache TUs with __DATE__/__TIME__ |
CCACHE_HARDLINK | 1 | Hardlink cached objects instead of copying — halves I/O on cache hits |
CCACHE_MAXSIZE | 2% of partition, clamped [10G, 100G] | Auto-sized to scratch partition |
RUSTC_WRAPPER | sccache | Rust compiler caching |
SCCACHE_CACHE_SIZE | 2% of partition, clamped [10G, 100G] | Same auto-sizing as ccache |
CMake integration: CMAKE_C_COMPILER_LAUNCHER=ccache and CMAKE_CXX_COMPILER_LAUNCHER=ccache are exported automatically.
openssh from Homebrew
The Brewfile installs openssh cross-platform (not just macOS) to avoid OpenSSL version
mismatches between the system ssh and Homebrew-linked libraries. On Linux, the system
ssh may link against a different OpenSSL than Homebrew’s, causing git push failures
when Homebrew’s git shells out to ssh. Brew’s openssh uses Homebrew’s OpenSSL consistently.
Source files
Toolchain source files live in install/cmake/toolchains/ — edit them there, not in the
deployed copies under $LOCAL_PLAT/. Re-deploy with:
bash ~/dotfiles/install/cmake.sh
Then wipe the CMake cache (rm -rf build/CMakeCache.txt build/CMakeFiles) for the changes
to take effect in an existing build directory.
Updating all packages
~/dotfiles/bootstrap.sh update # pull + refresh (install missing, skip current)
~/dotfiles/bootstrap.sh upgrade # update + brew upgrade + cargo upgrade
update refreshes tools without upgrading existing versions. upgrade additionally enables Homebrew upgrades and forces cargo-binstall to re-check for newer binaries. Both are idempotent — safe to run at any time.
PLAT isolation
PLAT (PLATform) is the per-architecture directory namespacing scheme this repo uses to make a single $HOME work across machines with different CPU architectures. It’s off by default because most users have one machine.
The decision in 30 seconds
Do you share $HOME across machines with different CPUs (NFS, etc.)?
├── No → leave DF_USE_PLAT=0 (default). Done.
└── Yes → set DF_USE_PLAT=1 on every machine that shares the home.
Each machine installs into ~/.local/$PLAT/ instead of ~/.local/.
One home, many machines, no clobbering.
DF_USE_PLAT=0 (default) | DF_USE_PLAT=1 | |
|---|---|---|
| Layout | flat ~/.local/{bin,brew,cargo,nvm,…} | per-PLAT ~/.local/$PLAT/{bin,brew,cargo,nvm,…} |
$LOCAL_PLAT | $HOME/.local | $HOME/.local/$PLAT |
| Capability flags | still applied (CPU-tuned -march, RUSTFLAGS, HOMEBREW_OPTFLAGS) | same |
| PATH entries | ~/.local/bin first | ~/.local/$PLAT/bin first, then ~/.local/bin |
| Disk per machine | one tree (~few GB) | one tree per PLAT (~few GB × N) |
| Right for | single laptop, workstation, VM | NFS-shared $HOME across heterogeneous CPUs (HPC, lab racks) |
Layouts side-by-side
DF_USE_PLAT=0 (default, flat) DF_USE_PLAT=1 (NFS-shared homes)
───────────────────────────── ────────────────────────────────────
~/.local/ ~/.local/
├── bin/ ├── plat_Darwin_arm64/
│ ├── chezmoi │ ├── bin/{chezmoi,uv,claude}
│ ├── uv │ ├── brew/ (Apple Silicon)
│ └── claude │ ├── cargo/bin/ (arm64 binaries)
├── brew/ (one prefix) │ └── nvm/ (arm64 node)
├── cargo/bin/ (host arch) ├── plat_Linux_x86-64-v3/
└── nvm/ │ ├── brew/ (AVX2 glibc)
│ └── ...
$_LOCAL_PLAT = ~/.local └── plat_Linux_x86-64-v4/ (AVX-512)
└── ...
$_LOCAL_PLAT = ~/.local/$_PLAT
(set per-shell from CPU detection)
Even with PLAT off, .plat_env.sh still sources at shell start so the host CPU gets -march=x86-64-v3, RUSTFLAGS=-C target-cpu=apple-m1, etc. Capability detection is independent of directory layout — only LOCAL_PLAT changes.
What PLAT directories look like
PLAT is a string of the form plat_{OS}_{cpu-target}. Examples:
plat_Darwin_arm64 # Apple Silicon
plat_Darwin_x86-64 # Intel Mac
plat_Linux_aarch64 # ARM Linux (Graviton, Ampere)
plat_Linux_x86-64-v4 # AVX-512 (Ice Lake+, Zen 4+)
plat_Linux_x86-64-v3 # AVX2 (Haswell+, Zen 2+)
plat_Linux_x86-64-v2 # SSE4.2 (Nehalem+)
Detection: shell startup scans ~/dotfiles/install/plat/plat_${OS}_*/ (highest level first), runs each spec’s .plat_check.sh, picks the first that exits 0, then sources .plat_env.sh for compiler flags.
Enabling PLAT isolation
Per-machine, persistent (recommended):
# Edit chezmoi data
chezmoi edit ~/.config/chezmoi/chezmoi.toml
# Set:
# use_plat = true
chezmoi apply
exec zsh -l # reload shell so $_LOCAL_PLAT picks up the new path
One-shot via env var:
DF_USE_PLAT=1 ~/dotfiles/bootstrap.sh
The env var is normalized — 1, true, yes, on (case-insensitive) all enable.
Disabling / migrating off PLAT
When you switch a machine from DF_USE_PLAT=1 back to flat, the old ~/.local/$PLAT/ tree becomes orphaned (multi-GB of cargo registry, nvm node versions, uv tools, etc., all stranded). One-shot cleanup:
# 1. Set DF_USE_PLAT=0 (or remove use_plat=true from chezmoi data)
# 2. Reload shell so the running session sees the flat layout
# 3. Run the decommission script:
bash ~/dotfiles/install/plat-decommission.sh
The script is standalone — never invoked by bootstrap.sh (including upgrade mode), to prevent accidental data loss. Safety guarantees:
- Refuses to run if
DF_USE_PLAT=1is currently set in the environment (won’t nuke the active install) - Asks for confirmation before deleting (skip with
DF_FORCE=1) - Idempotent — running with no
~/.local/plat_*/dirs is a no-op - After cleanup, re-run
~/dotfiles/bootstrap.shto repopulate the flat layout
Failure modes PLAT exists to prevent
If you skip PLAT but actually share $HOME across architectures, you get one of these:
- Wrong-arch binary on PATH — Linux machine sees Apple Silicon
~/.local/bin/uv; runs and immediately segfaults withBad CPU typeorcannot execute binary file. - Cargo registry corruption — two machines share
~/.local/cargo/registry/and race-update the index Git repo. Eventually one machine’scargo buildfails with “object file is broken.” - nvm node-version collisions — one machine’s
node v25.9.0is x86_64 ELF; another machine sees the same path containing arm64.node --versionsegfaults. - Brew prefix incompatibility — Brew’s bottle relocation embeds the prefix path in binaries. Running
brew install fooon machine A then trying to usefooon machine B without re-installing fails because the embedded RPATH is for A’s libgcc.
PLAT is the heavy hammer that solves all of these by giving each architecture its own tree. The cost is disk space (a few GB × number of machines) and one extra path segment in $_LOCAL_PLAT.
Why opt-in by default
Most people have one machine. The per-PLAT directory adds a layer of indirection, breaks tools that hard-code their own install location (uv self update was the canonical bug), and makes default tutorials more confusing. The mainstream answer to “what about binaries on shared $HOME?” in the broader ecosystem is don’t share that part of $HOME (move ~/.local to local disk per host). PLAT exists for the cases where that’s not an option — typically HPC NFS where you can’t.
See install/_lib.sh (the ### PLATFORM ### block) for the implementation.
Auth (API tokens)
install/auth.sh is a guided helper for the API tokens this repo’s tools need. It maintains ~/.<service>.env files (chmod 600) — sourced automatically by install/_lib.sh on every install run and by your login shell.
Quick reference
bash ~/dotfiles/install/auth.sh # walk every service interactively
bash ~/dotfiles/install/auth.sh status # current state, no prompts
bash ~/dotfiles/install/auth.sh huggingface # set/update one
bash ~/dotfiles/install/auth.sh gh # `gh auth login` (browser)
bash ~/dotfiles/install/auth.sh help # service list
# Or as part of bootstrap:
DF_DO_AUTH=1 ~/dotfiles/bootstrap.sh
Service registry
| Service | Env var | File | Used for | Skip if |
|---|---|---|---|---|
| github | GITHUB_TOKEN | ~/.github.env | cargo-binstall rate limits, Homebrew rate limits, gh CLI fallback | you don’t bulk-binstall from GitHub releases (or use the gh-derive trick below) |
| anthropic | ANTHROPIC_API_KEY | ~/.anthropic.env | Anthropic SDK, agents using api.anthropic.com directly | you only use Claude via Pro / Claude Code OAuth |
| openai | OPENAI_API_KEY | ~/.openai.env | OpenAI SDK, Codex CLI in API mode | you only use Codex via ChatGPT login |
| cloudflare | CLOUDFLARE_API_TOKEN | ~/.cloudflare.env | OpenTofu in infra/, Cloudflare MCP via API, R2/Pages | you don’t deploy infra/ via OpenTofu (the Cloudflare MCP can use OAuth) |
| huggingface | HF_TOKEN | ~/.huggingface.env | mlx-lm gated models, transformers | you don’t pull gated models or private repos |
Plus gh auth login (browser flow) — required for the GitHub MCP server consumed by both Claude and Codex (auth=gh in mcp-servers.txt). gh stores its token in macOS keychain / Linux secret service, not in an env file.
How tokens get loaded
Walk auth.sh ─writes─► ~/.<service>.env (chmod 600)
│
│ sourced on every install run
▼
install/_lib.sh ◄─sources─ for f in ~/.*.env; do . "$f"; done
│
│ exported into the shell environment
▼
install scripts see GITHUB_TOKEN, HF_TOKEN, etc. as env vars.
Same files are also sourced by your shell profile so interactive
sessions inherit them — no need to `source` manually after setup.
After setting a token, open a new shell (or source ~/.<svc>.env) to use it in your current session.
Per-prompt UX
Each service prompt shows status, create-URL, scope hint, file path, and a “skip if” note. Then either [k]eep / [u]pdate / [d]elete (when set) or “Enter token / Enter to skip” (when empty). Tokens are masked everywhere — only the last 4 characters appear (e.g. ...mqTO). Input is hidden via stty -echo.
github (GITHUB_TOKEN)
GitHub PAT (cargo-binstall, Homebrew rate limits, gh fallback)
create: https://github.com/settings/tokens
scopes: fine-grained no-permission (rate limits only) OR repo (private clones)
skip if: you don't bulk-binstall from GitHub releases — or press G to derive from `gh auth token`
file: /Users/cade/.github.env
status: empty
Enter GITHUB_TOKEN, [G] to derive from `gh auth token`, or Enter to skip:
After a walk, you get a tally:
Summary
set: 2
updated: 0
kept: 1
deleted: 0
skipped: 2
The gh-derive trick (GITHUB_TOKEN)
gh auth login already stores a token in your OS keychain. Rather than maintain a second token, point ~/.github.env at the keychain dynamically:
# ~/.github.env
export GITHUB_TOKEN="$(gh auth token 2>/dev/null)"
Now cargo-binstall etc. always see the current keychain token, and gh auth refresh automatically picks up everywhere.
The auth.sh prompt offers this with [G] when github is empty and gh auth status succeeds. Selecting it writes exactly that one-liner.
Adding a new service
The registry is one constant in install/auth.sh. Add a row with:
name|ENV_VAR|.env_file_basename|short description|create_url|scopes hint|skip-if hint
Example for adding OpenRouter:
"openrouter|OPENROUTER_API_KEY|.openrouter.env|OpenRouter token (openrouter/ models)|https://openrouter.ai/keys|—|you don't use OpenRouter-routed models"
Now bash auth.sh status, bash auth.sh openrouter, and the walk all include it. No code changes needed.
File security
- All env files are chmod 600 (owner-only).
- Tokens are never echoed in plaintext — only masked tails.
- The bash glob
for _envfile in "$HOME"/.*.envin_lib.sherrors silently if no files match (no leakage). - A global pre-push gitleaks hook scans the commits being pushed for accidental token leakage before they reach a remote, across every repo on the machine (via
core.hooksPath). Source:home/dot_config/git/hooks/executable_pre-push→ deployed to~/.config/git/hooks/pre-push. See Troubleshooting → git push blocked by gitleaks if it ever blocks a push.
Scratch space
Some shared filesystems give you a tiny home quota and a much larger “scratch” partition (HPC clusters, lab racks, certain NAS setups). The bootstrap can transparently redirect heavy directories to scratch via symlinks, so the multi-GB Homebrew prefix and tool caches never touch NFS.
You don’t need this if your $HOME quota is fine. Skip the rest of this page.
How it works
install/scratch.sh (run as bootstrap step 0) symlinks selected $HOME directories into $DF_SCRATCH/.paths/. Existing contents are moved over before the symlink replaces the original directory.
$HOME/ $DF_SCRATCH/.paths/
├── .local ──symlink──▶ ├── .local/ ◀── PLAT dirs, brew, cargo
├── .cache ──symlink──▶ ├── .cache/ ◀── ccache, sccache, uv cache
├── .npm ──symlink──▶ ├── .npm/
├── .nv ──symlink──▶ ├── .nv/ ◀── NVIDIA shader cache
├── .vscode ──symlink──▶ ├── .vscode/
├── .vscode-server ─symlink──▶ ├── .vscode-server/
├── .cursor ──symlink──▶ ├── .cursor/
├── .cursor-server ─symlink──▶ ├── .cursor-server/
├── .oh-my-zsh ──symlink──▶ └── .oh-my-zsh/
│
├── .claude/ ◀── NOT symlinked (chezmoi-managed; symlinking
│ would orphan history files)
├── dotfiles/ ◀── real dir, version controlled
└── .config/ ◀── real dir, small files
Configuring
Either set DF_SCRATCH before running bootstrap:
DF_SCRATCH=/scratch/$USER ~/dotfiles/bootstrap.sh
…or pre-create a ~/scratch symlink and let bootstrap auto-detect it:
ln -s /local/disk/$USER ~/scratch
~/dotfiles/bootstrap.sh
| Env var | Default | What it does |
|---|---|---|
DF_SCRATCH | (unset) | Path to scratch root. Setting this enables scratch mode. |
DF_SCRATCH_LINK | ~/scratch | Symlink in $HOME pointing at scratch. Bootstrap creates this if DF_SCRATCH is set. |
DF_LINKS | ~/.local:~/.cache:~/.vscode:~/.vscode-server:~/.cursor:~/.cursor-server:~/.nv:~/.npm:~/.oh-my-zsh:~/.oh-my-zsh-custom | Colon-separated list of dirs to symlink to scratch. Override to customize. |
DF_DO_SCRATCH | 1 (install mode), 0 (update/upgrade) | Skip scratch setup entirely. |
What NOT to symlink
These look tempting but are traps:
~/.claude/— chezmoi manages files here. If symlinked,chezmoi applyreplaces the symlink with a real directory containing only managed files, orphaning all your conversation history, sessions, and file-history on scratch. Already excluded fromDF_LINKSdefaults.~/.config/— small, fast, and chezmoi-managed. Many tools assumeXDG_CONFIG_HOMEis local-disk-fast (e.g. shell startup reads it constantly).~/dotfiles/— the repo itself. Cloned to$HOMEdirectly so editor “open file” dialogs and IDE indexing work normally.~/.ssh/— security boundary. Local disk only.
Filesystem caveats
- tmpfs scratch is detected and warned about — contents are lost on reboot. Fine for ephemeral state, fatal for the Homebrew prefix.
- Cross-filesystem moves can be slow on first bootstrap (existing
~/.localmay be tens of GB). Subsequent runs are no-ops. - NFS open-file locks sometimes leave
.nfs*silly-rename files behind during the move; the script logs a warning but doesn’t fail.
Re-running
scratch.sh is idempotent. If a path is already a symlink to the right target, it’s left alone. If it’s a real directory with new content, the script moves the new content and re-symlinks. If it’s a symlink pointing somewhere unexpected, the script logs a warning and skips (won’t silently overwrite an admin-set link).
To opt out without unwinding the symlinks (just stop redirecting new dirs):
DF_DO_SCRATCH=0 ~/dotfiles/bootstrap.sh
To fully unwind (move data back to real $HOME), do it manually — the script doesn’t ship a “decommission scratch” mode.
Overlays
An overlay is a separate repo (typically private) that extends this base dotfiles without forking. Overlays live next to the base in $DF_ROOT/dotfiles-*/ and get discovered automatically — their package lists, install scripts, claude skills, and Codex skills compose with the base.
Use overlays for:
- Personal/private content that shouldn’t ship in the public repo (
dotfiles-personal/) - Org-specific setup (
dotfiles-acme/,dotfiles-lab/) - Hardware-specific extras (
dotfiles-nvidia/for CUDA toolkits, MCPs, kernels)
Discovery model
The base _lib.sh defines DF_OVERLAYS (an array of paths to overlay roots) and overlay_package_files() (a helper that returns base-first-then-overlays paths for any package list filename).
~/dotfiles/ ← base, public
└── packages/mcp-servers.txt (5 entries: cloudflare, github, openaiDeveloperDocs, context7, blender)
~/dotfiles-nvidia/ ← overlay, private
└── packages/mcp-servers.txt (NVIDIA-internal MaaS entries)
│
│ install/claude.sh + install/codex.sh:
│ while IFS= read -r f; do
│ _register_mcps_from "$f" # claude.sh
│ _emit_mcp_blocks_to ... # codex.sh
│ done < <(overlay_package_files "mcp-servers.txt")
│
▼
Effective merged list (base first, then each overlay sorted) — same list
consumed by both Claude (`claude mcp add`) and Codex (`[mcp_servers.*]`).
The merge is append-only — overlays add to the base, they don’t replace it. Order is base, then overlays in lexicographic path order.
What an overlay can provide
| Path in overlay | Effect |
|---|---|
packages/cargo.txt | additional Rust crates installed by install/rust.sh |
packages/mcp-servers.txt | additional MCP servers registered by install/claude.sh and install/codex.sh |
packages/claude-plugins.txt | additional Claude plugins installed |
packages/<other>.txt | discovered via overlay_package_files() — pattern works for any list-style file |
home/dot_claude/CLAUDE.md | appended to ~/.claude/CLAUDE.md via the chezmoi template |
home/dot_claude/skills/<name>/SKILL.md | deployed to ~/.claude/skills/<name>/ by install/claude.sh |
install/auth.sh | runs alongside the base auth walk during step 7.5 (post-base auth) |
install/<other>.sh | source _lib.sh and use the same conventions; invoked from the overlay’s bootstrap |
bootstrap.sh | runs as the base bootstrap step 8 (after everything else) |
The base intentionally has no built-in awareness of any specific overlay — discovery is purely by directory glob (dotfiles-*/).
Creating an overlay
# 1. Create the repo somewhere accessible (or just a local dir):
mkdir -p ~/dotfiles-mine
cd ~/dotfiles-mine
git init
# 2. Add a package file or two:
mkdir -p packages
cat > packages/cargo.txt <<'EOF'
# my private cargo additions
hyperfine
flamegraph
EOF
# 3. Optionally, a bootstrap to do per-overlay setup:
cat > bootstrap.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
source "$DF_ROOT/install/_lib.sh" # base helpers (log_info, etc.)
log_section "dotfiles-mine"
# ... your custom logic ...
EOF
chmod +x bootstrap.sh
# 4. Symlink (or clone) it next to the base:
ln -s ~/dotfiles-mine ~/dotfiles/dotfiles-mine
# 5. Re-run the base bootstrap. Step 8 picks up your overlay automatically.
~/dotfiles/bootstrap.sh
The directory name must start with dotfiles- for the glob to find it. Common names: dotfiles-personal, dotfiles-work, dotfiles-{laptop,desktop,server}, dotfiles-{nvidia,amd,intel}.
chezmoi integration
Overlays don’t usually own their own chezmoi root — instead, the base home/ template references overlay files via glob:
{{ glob (joinPath .chezmoi.workingTree "dotfiles-*/packages/mcp-servers.txt") }}
This pattern is used by home/run_onchange_*.sh.tmpl scripts, so chezmoi notices when any overlay’s package file changes (not just the base) and re-fires the install script.
For chezmoi-managed content (skills, claude/codex configs), the base’s chezmoi templates have {{ if (stat ...) }} guards that pull the overlay file’s contents in if present.
Why overlays vs forks
A fork makes you carry every base change into your private tree forever. An overlay lets you git pull the base independently and keep your private stuff strictly additive. Conflicts only happen if the base removes something your overlay depended on (rare; the discovery contract is stable).
For one-off per-machine tweaks that aren’t worth a whole overlay, see Managing dotfiles → Customizing per-machine. Overlays are the right answer when the tweak is a coherent set of files you’d commit together.
Day-to-day workflow
Update and upgrade
~/dotfiles/bootstrap.sh update # pull latest + refresh tools (no brew upgrade)
~/dotfiles/bootstrap.sh upgrade # update + brew upgrade + cargo upgrade
~/dotfiles/bootstrap.sh # full install (same as first run, idempotent)
update pulls the repo, applies chezmoi, refreshes zsh plugins, and re-runs all install scripts (which skip already-installed tools). upgrade does the same but also enables Homebrew upgrades and forces cargo-binstall to re-check for newer binaries.
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 # zsh login shell
chezmoi edit ~/.bash_profile # bash login shell
chezmoi edit ~/.gitconfig
Or edit the source directly and apply:
$EDITOR ~/dotfiles/home/dot_zshrc.tmpl
$EDITOR ~/dotfiles/home/dot_zprofile.tmpl
$EDITOR ~/dotfiles/home/dot_bash_profile.tmpl
chezmoi apply
Preview before applying: chezmoi diff
Sync dotfiles from the repo
chezmoi update # git pull + chezmoi apply
AeroSpace config (v2)
Window-management docs are now in AeroSpace window management.
Update AI agent instructions
Claude and Codex now diverge intentionally:
chezmoi edit ~/.claude/CLAUDE.md
chezmoi edit ~/.codex/AGENTS.md
Use ~/.claude/CLAUDE.md for Claude-specific memory and ~/.codex/AGENTS.md for Codex-specific guidance. Keep only genuinely shared preferences aligned.
Claude Code’s status line is a custom bash script at home/dot_claude/executable_statusline.sh (no npm dependency). Edit it with chezmoi edit ~/.claude/statusline.sh. The header comment documents the shape; DEBUG=1 env var dumps parsed input + intermediate values to stderr.
Codex also has global skills and rules (edit source-of-truth in the repo):
$EDITOR ~/dotfiles/home/dot_codex/create_config.toml
$EDITOR ~/dotfiles/home/dot_codex/rules/dotfiles.rules
chezmoi apply
~/dotfiles/install/codex.sh sync-config
Codex binary/config health commands:
~/dotfiles/install/codex.sh upgrade # install latest binary + sync config + healthcheck
~/dotfiles/install/codex.sh sync-config # sync managed config; preserve runtime trust sections
~/dotfiles/install/codex.sh check # verify binary, profiles, and rules
Skills live under home/dot_claude/skills/ in the repo, apply to ~/.claude/skills/,
and reach Codex/opencode/pi through the ~/.agents/skills symlink.
Custom domain skills included:
web-shippingsimulation-labcompiler-workbenchgame-systems
Custom Codex themes live under home/dot_codex/themes/ and sync to ~/.codex/themes/:
neon-noirsunburst-candyminty-terminal
Useful Codex commands after updating:
codex --profile fast
codex --profile review
codex --profile deep
codex # default profile is already danger-full-access
codex -c 'tui.theme="neon-noir"'
codex -c 'tui.theme="sunburst-candy"'
codex -c 'tui.theme="minty-terminal"'
codex mcp list
codex execpolicy check --pretty --rules ~/.codex/rules/dotfiles.rules -- git status
codex '$env-reconciler Map this repository and propose the first validation step.'
codex '$simulation-lab Define state variables and a minimal validation case for this model.'
Codex schema note: profiles are delta-only overlay files at ~/.codex/<name>.config.toml
with top-level keys (Codex 0.134+); the old [profiles.*] tables in config.toml are
ignored. Managed sources: home/dot_codex/{deep,review,fast}.config.toml.
Default Codex mode is intentionally autonomous: sandbox_mode = "danger-full-access"
with granular approvals (approval_policy = { granular = { rules = true, ... } }) so the
execpolicy prompt rules (rm, git reset –hard, git push) stay live. Use -p deep
(workspace-write) or -p review (read-only) to opt into tighter sandboxes.
Add an env var or PATH entry
Edit both home/dot_zprofile.tmpl and home/dot_bash_profile.tmpl (they should stay identical). 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.
AeroSpace (v2)
This is the canonical reference for macOS window management in this dotfiles repo.
Source of truth
$EDITOR ~/dotfiles/home/dot_aerospace.toml
chezmoi apply ~/.aerospace.toml
aerospace reload-config
Design principles
- Direct hotkeys for primary actions (no leader-mode dependency)
- No hardcoded workspace-to-monitor assignment
- No automatic app-to-workspace routing
- Tight grid (zero gaps) with predictable normalization
Main keymap
alt + ←/↓/↑/→: focus windowalt + shift + ←/↓/↑/→: move windowcmd + alt + ←/↓/↑/→: join-with directionalt + -/alt + =: resize smart-50/+50alt + /: cyclelayout tiles horizontal verticalalt + ,: cyclelayout accordion horizontal verticalalt + f: AeroSpace fullscreenalt + shift + f: macOS native fullscreenalt + tab: workspace back-and-forthalt + 1..9: switch workspacealt + shift + 1..9: move node to workspace and followcmd + alt + 1..9: move node to workspace without followingalt + pageUp/pageDown: focus monitor next/prev (wrap)alt + shift + pageUp/pageDown: move workspace to monitor next/prev (wrap)
Service mode
- Enter:
alt + shift + ; esc: reload config + return to mainr: flatten workspace tree + return to mainf: toggle floating/tiling + return to mainbackspace: close all windows but current + return to main
Local AI coding
Local LLM inference on macOS Apple Silicon (M-series) — no API keys, no rate limits, no cloud — used as the default backend for opencode and pi (and as a generic OpenAI-compatible endpoint for anything else).
Overview
| Layer | Tool | Where it lives |
|---|---|---|
| Server | mlxserve (mlx-openai-server) | LaunchAgent dev.cade.mlxserve (KeepAlive) or foreground shell function; port 8080, OpenAI-compat + tool calling |
| Server (fallback) | Ollama | LaunchAgent, port 11434, OpenAI-compat |
| Client | opencode, pi | Both point at localhost:8080/v1 by default on macOS |
| Cloud | Anthropic, OpenAI | Available everywhere via ANTHROPIC_API_KEY / OPENAI_API_KEY |
MLX is the primary backend because it’s roughly 2-3× faster than Ollama
(llama.cpp) on the M3 Max for the same quants, and mlx-openai-server adds
OpenAI tool-call parsing on top — which mlx_lm.server upstream still lacks.
Ollama remains installed as a plain fallback.
Quick start
# LaunchAgent (preferred — survives terminal close, KeepAlive):
mlxstart # launchctl bootstrap dev.cade.mlxserve
mlxstatus # is it running?
mlxstop
# Or foreground in a terminal:
mlxserve # default: Qwen3.6-27B 8-bit (served as "qwen3.6-27b")
mlxserve qwen3.6-35b-a3b # MoE alternative — fast tokens (3B active)
mlxserve coder-next # Qwen3-Coder-Next 80B/3B MoE (no thinking)
# Then launch any client:
opencode # TUI agent, full tool-calling loop
pi # TUI agent, full tool-calling loop
All requests use the served-model-name qwen3.6-27b regardless of which
physical model is loaded — client configs stay stable when you swap models.
mlxserve and mlx-openai-server
mlxserve is a shell function (defined in both .zshrc and .bashrc) that
starts mlx-openai-server with the right parsers for the chosen model:
mlx-openai-server launch \
--model-type lm \
--model-path unsloth/Qwen3.6-27B-MLX-8bit \
--served-model-name qwen3.6-27b \
--tool-call-parser qwen3_coder \
--enable-auto-tool-choice \
--reasoning-parser qwen3_5 \
--kv-bits 8 --kv-group-size 64 \
--host 127.0.0.1 --port 8080
The parser flags are critical: opencode and pi are tool-call-heavy, and the
upstream mlx_lm.server does not emit tool_calls[] in OpenAI format
(ml-explore/mlx-lm#1096).
mlx-openai-server adds parser layers that translate model output into the
standard format. Qwen3.6 emits Qwen3-Coder’s XML tool-call wire format, so
the tool parser is qwen3_coder even on non-Coder variants; the reasoning
parser (qwen3_5) strips <think> blocks before clients see the output.
Override the port with MLX_PORT=9000 mlxserve.
Pre-pulled models
Models live in packages/mlx-models.txt:
unsloth/Qwen3.6-27B-MLX-8bit # primary (~35 GB, 256K ctx, reasoning-tuned)
# mlx-community/Qwen3.6-35B-A3B-8bit # MoE alternative — pull on demand
# mlx-community/Qwen3-Coder-Next-8bit# max tool-call throughput (~85 GB)
Pre-pull the default set in one shot:
bash ~/dotfiles/install/local-llm.sh pull-models
This is opt-in (the default local-llm.sh run only verifies binaries —
pulling ~35 GB of models on every bootstrap would be unfriendly). The
commented entries are one mlxpull <alias> away.
HF_HOME is set by .zprofile to $_LOCAL_PLAT/.cache/huggingface, so
weights live on scratch when scratch is configured.
Per-tool config
Both coding agents are configured to use localhost:8080/v1 as their
default backend on macOS. Each one lives under chezmoi:
| Tool | Default config | AGENTS file |
|---|---|---|
| opencode | ~/.config/opencode/opencode.json (+ plugin/git-context.ts) | ~/.config/opencode/AGENTS.md |
| pi | ~/.pi/agent/{settings,models}.json (+ themes/dotfiles.json) | ~/.pi/agent/AGENTS.md |
Both AGENTS files (plus Claude’s CLAUDE.md and Codex’s AGENTS.md)
include a shared partial — see Agent guidance. Cloud model pins
are single-sourced in home/.chezmoidata.toml ({{ .models.opus }} etc.).
Switching to cloud
# opencode — switch agent or model in the TUI
/agent plan # plan agent runs Opus
/model anthropic/claude-sonnet-4-6
# pi — Ctrl+L (or /model)
/model anthropic/claude-sonnet-4-6
API keys come from ~/.<service>.env files (written by bash auth.sh),
sourced into the shell by ~/.zprofile.
Ollama (fallback)
Installed via Homebrew (brew "ollama"). Managed as a LaunchAgent on macOS
— starts at login at http://127.0.0.1:11434. No model fleet is maintained
for it; an ad-hoc pull (ollama pull qwen3-coder:30b) is one command away.
(The old context-boosted alias machinery was removed — nothing consumed it.)
run_onchange hooks
| Trigger file | Script re-run |
|---|---|
packages/pip.txt | install/local-llm.sh (verifies binaries) |
home/dot_config/opencode/opencode.json.tmpl | install/opencode.sh (binary check) |
chezmoi update after pulling dotfile changes re-verifies the setup.
Agent guidance
Four different AI coding tools (Claude Code, Codex, opencode, pi) each expect their own AGENTS.md / CLAUDE.md file. Most of the content is the same — user background, communication style, engineering principles, tool preferences. The differences are the per-tool addenda (skill systems, MCP usage, tool-call quirks, etc.).
The shared partial
home/.chezmoitemplates/agents-common.md holds the common content. Each
tool’s .tmpl file pulls it in with one line:
{{ template "agents-common.md" . }}
A typical wrapper looks like:
# AGENTS.md
This is the global memory for <tool>. Common guidance lives in the shared
partial; <tool>-specific notes follow.
{{ template "agents-common.md" . }}
## <Tool>-specific
- ...tool quirks, MCP setup, edit modes, etc...
voice-common.md
home/.chezmoitemplates/voice-common.md holds tone/communication and
estimate conventions — deliberately split out of agents-common.md so it can
load at different levels per tool: Claude gets it via the cade output style
(system-prompt level), while the Codex/opencode/pi wrappers include it
directly next to agents-common.md. Keeping it out of agents-common.md
means Claude never loads the voice guidance twice.
Where each file lives
| Tool | Source (chezmoi) | Deployed to |
|---|---|---|
| Claude Code | home/dot_claude/CLAUDE.md.tmpl | ~/.claude/CLAUDE.md |
| Codex | home/dot_codex/AGENTS.md.tmpl | ~/.codex/AGENTS.md |
| opencode | home/dot_config/opencode/AGENTS.md.tmpl | ~/.config/opencode/AGENTS.md |
| pi | home/dot_pi/agent/AGENTS.md.tmpl | ~/.pi/agent/AGENTS.md |
All four render through the same partial — edit agents-common.md once and
chezmoi apply propagates everywhere.
Adding a new tool
- Drop
home/<tool-config-path>/AGENTS.md.tmpl(or whatever the tool calls it) with the wrapper shown above. - Add a
## <Tool>-specificsection at the bottom for anything the partial doesn’t cover. chezmoi applydeploys it.
No bootstrap.sh changes needed — chezmoi apply is step 2 of every bootstrap.
Editing the shared content
Edit home/.chezmoitemplates/agents-common.md directly. The change takes
effect on every tool the next time they read their config (most pick up
file changes on session start; some are eager).
Project-level overrides
Most of these tools also walk up from the current working directory looking for a project-local AGENTS.md / CLAUDE.md. Those override or augment the global file — write project-specific guidance there, not in the partial.
Skills (shared across tools)
Skills live in one place: home/dot_claude/skills/ → deployed to
~/.claude/skills. A chezmoi-managed symlink ~/.agents/skills →
~/.claude/skills exposes the same tree to Codex, opencode, and pi (all
three scan ~/.agents/skills; opencode also reads ~/.claude/skills
directly). One SKILL.md edit propagates to every tool on chezmoi apply.
Memory layers
Three layers, set up by install/memory.sh (bootstrap step 6.6, DF_DO_MEMORY):
| Layer | Store | Search | Synced? |
|---|---|---|---|
| L1 auto-memory | ~/.claude/projects/<proj>/memory/ (markdown) | loaded each session; also indexed by qmd | no (per-machine) |
| L2 knowledge base | ~/kb git repo (markdown) | qmd — hybrid BM25 + local GGUF embeddings + rerank, MCP daemon on localhost:8181 | yes (git remote) |
| L3 session history | every agent’s transcripts (Claude Code, Codex, opencode, pi) | cass — hybrid BM25 + local ONNX embeddings (nomic-embed), CLI/history-search skill | no (per-machine) |
Search indexes always rebuild locally (~/.cache/qmd, ~/.cache/cass — on
scratch when configured); only ~/kb and the dotfiles repo sync across
machines. Daemons: LaunchAgents dev.cade.qmd / dev.cade.cass-watch on
macOS, lazy-start from the shell profiles on Linux.
Re-index after bulk changes: bash install/memory.sh reindex (forces qmd
re-embed and a full cass rebuild). Agent-facing usage rules live in the
## Memory layers section of agents-common.md.
Troubleshooting
Quick reference for when things go wrong. Check here before digging into scripts.
Tool not found after bootstrap
echo "$_PLAT" "$_LOCAL_PLAT" # capability + install root
ls "$_LOCAL_PLAT/bin/" # chezmoi, uv, claude should be here
ls "$_LOCAL_PLAT/cargo/bin/" # fd, sd, zoxide, etc.
which fd # should point under $_LOCAL_PLAT
$_LOCAL_PLAT is $HOME/.local by default (flat layout) or $HOME/.local/$_PLAT when PLAT isolation is enabled. If $_PLAT or $_LOCAL_PLAT is empty, .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/.bash_profile adds from the highest installed version. If node is missing in a script, either:
# Option 1: source profile at the top of your script (zsh)
source ~/.zprofile
# Option 1b: source profile at the top of your script (bash)
source ~/.bash_profile
# Option 2: use the full path
NODE="$NVM_DIR/versions/node/$(ls $NVM_DIR/versions/node | sort -V | tail -1)/bin/node"
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:
DF_NAME="Your Name" DF_EMAIL="[email protected]" chezmoi init
chezmoi diff shows unexpected changes
Another program modified a managed file. Common culprits:
uvauto-adds source lines to.zshrc/.bashrcfor itsbin/envfiles- Claude Code updates
~/.claude/settings.jsonwhen plugins are installed - Other tools may modify shell configs without asking
Options:
chezmoi diff # see what changed
chezmoi apply --force # overwrite with repo version (safe for shell configs)
chezmoi add ~/.claude/settings.json # pull the live version into the repo (for config files)
For shell configs (.zshrc, .zprofile, .bash_profile), always use chezmoi apply --force to restore the clean template. These files should never be manually edited.
PATH order is wrong — wrong binary is resolving
Expected priority (highest to lowest). $_LOCAL_PLAT collapses to $HOME/.local in flat-mode (default).
$_LOCAL_PLAT/cargo/bin Rust tools (fd, sd, zoxide, bat, rg, etc.)
$_LOCAL_PLAT/nvm/.../bin Node.js (highest installed version)
$_LOCAL_PLAT/bin chezmoi, uv, claude, codex, uv-tool entrypoints
~/.local/bin arch-neutral scripts (collapses to $_LOCAL_PLAT/bin in flat mode — deduped via typeset -U)
/opt/homebrew/bin Homebrew (macOS) — also where rustup lives
/opt/homebrew/sbin Homebrew sbin
/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.
The other classic shadowing footgun: legacy binaries at ~/.local/bin/<tool> from before a layout migration. The [[ -x "$ARCH_BIN/<tool>" ]] install checks in current scripts catch most of these, but if <tool> --version shows an unexpectedly old version, check ls ~/.local/bin/<tool>* for backups (*.preplat-bak.* or stale binaries) and delete them.
Cloudflare Pages build failing
Check the build log via the API:
ACCOUNT="YOUR_CLOUDFLARE_ACCOUNT_ID"
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.
Duplicate PLAT paths in PATH (both v3 and v4 showing up)
Only relevant with DF_USE_PLAT=1. Fixed in current versions — .zprofile/.bash_profile resolve ~/.local symlinks before setting _LOCAL_PLAT so PATH entries use the same physical path.
If you upgraded from before that fix:
chezmoi apply ~/.zprofile ~/.bash_profile
exec zsh -l # or: exec bash -l
echo "$PATH" | tr ':' '\n' | grep plat # all entries should share the same PLAT prefix
In flat mode (DF_USE_PLAT=0, the default), this failure mode doesn’t apply — there’s no $PLAT segment in $_LOCAL_PLAT.
Lost shell history
Zsh history lives at ~/.zsh_history (the conventional default; survives any ~/.local cleanup). Bash history at ~/.bash_history. The bash sidecar command log (richer: timestamps, exit codes, cwd) at ~/.bash_log — search via bash_log_search <pattern>.
If you have history under the old location (~/.local/state/{zsh,bash}/), one-time migrate:
[ -f ~/.local/state/zsh/history ] && mv ~/.local/state/zsh/history ~/.zsh_history
[ -f ~/.local/state/bash/history ] && mv ~/.local/state/bash/history ~/.bash_history
[ -f ~/.local/state/bash/log ] && mv ~/.local/state/bash/log ~/.bash_log
Migrating off PLAT isolation
If you set up with DF_USE_PLAT=1 and want to switch to flat (or vice-versa), the layout in ~/.local/ is stable as long as one mode is active — but switching strands GBs in the unused tree. Decommission tool:
# After setting DF_USE_PLAT=0 (or removing use_plat=true from chezmoi data):
bash ~/dotfiles/install/plat-decommission.sh
Refuses to run if DF_USE_PLAT=1 is currently set (won’t nuke the active install). See PLAT isolation for the full migration story.
Brew zsh tab completion leaves remnant characters (Linux)
Symptom: after pressing Tab, stale characters remain on the line instead of being erased.
Root cause chain:
- Brew zsh’s RUNPATH loads Homebrew’s own glibc (
brew/opt/glibc/lib/libc.so.6) - Homebrew’s glibc ships no
lib/locale/data →setlocale()silently falls back toC/ASCII - In the C locale,
wcwidth()returns byte counts instead of display columns - Every cursor-position calculation in ZLE/completion is off → artifacts
Confirm by checking the codeset inside brew zsh:
zsh --no-rcs -c 'zmodload zsh/langinfo; echo $langinfo[CODESET]'
# broken: ANSI_X3.4-1968
# working: UTF-8
Fix: linux-packages.sh generates en_US.UTF-8 locale data for brew’s glibc into
$LOCAL_PLAT/locale/ using brew’s own localedef. The shell profiles export LOCPATH
pointing there so brew zsh picks it up at startup.
If you installed before this fix:
# Regenerate locale data
bash ~/dotfiles/install/linux-packages.sh
# Apply updated shell profiles (adds LOCPATH export)
chezmoi apply ~/.zprofile ~/.bash_profile
# Open a new login shell and verify
exec zsh -l
zsh --no-rcs -c 'zmodload zsh/langinfo; echo $langinfo[CODESET]' # UTF-8
Test suite: bash ~/dotfiles/tests/test-locale.sh
[email protected] build fails on Linux (uuid or test_datetime errors)
Python 3.14 from Homebrew has build issues on some Linux systems:
- UUID module detection failure - configure detects libuuid but the build fails
- test_datetime hangs during PGO - Profile-guided optimization runs the test suite, but
test_datetimehangs on some CPUs (timezone-related)
Fix: Patches are applied automatically by install/patch-homebrew-python.sh during bootstrap. If you need to re-apply manually:
bash ~/dotfiles/install/patch-homebrew-python.sh
brew reinstall --build-from-source [email protected]
The patches:
- Set
py_cv_module__uuid=n/ato disable the uuid module - Patch Makefile’s
PROFILE_TASKto skiptest_datetimeduring PGO
Environment variables in .zprofile/.bash_profile prevent Homebrew from auto-updating and overwriting these patches:
HOMEBREW_NO_AUTO_UPDATE=1- prevents tap updatesHOMEBREW_NO_INSTALL_FROM_API=1- forces local formula usage
git push blocked by gitleaks (“secrets detected”)
A global pre-push hook scans the commits being pushed for secrets with gitleaks and refuses the push if it finds any. This is the safety net that keeps tokens and private keys out of remote history — see Authentication → File security.
How it’s wired:
brew "gitleaks"(inpackages/Brewfile) installs the scanner.- The hook lives at
home/dot_config/git/hooks/executable_pre-push, deployed by chezmoi to~/.config/git/hooks/pre-push. ~/.gitconfigsetscore.hooksPath = ~/.config/git/hooks, so it applies to every repo on the machine, not just dotfiles.- It scans only the commits being pushed (a new branch is scanned against
--remotes), not the full history, so it stays fast. - If gitleaks isn’t installed yet, the hook prints a warning and exits cleanly rather than blocking you.
When a push is blocked, the hook prints the exact --log-opts range it flagged.
Review the finding:
# Re-run the scan the hook ran (range is printed in the failure message)
gitleaks git --log-opts="<remote_sha>..<local_sha>"
# Or scan the entire repo history
gitleaks git --no-banner
If it’s a real secret: rotate it, then rewrite the offending commit(s) to remove
it before pushing (a --no-verify push would leak it to the remote). If it’s a
confirmed false positive, add a gitleaks allowlist entry rather than
disabling the hook.
Emergency bypass (use only when you’re certain there’s no secret):
git push --no-verify
Don’t disable the hook permanently — core.hooksPath is global precisely so the
protection can’t be forgotten on a per-repo basis.
Environment variables
Complete reference for DF_* variables and the tool-standard ones this repo cares about. All DF_* flags are read in install/_lib.sh or bootstrap.sh. Set in your shell, prepend to a single command, or persist via chezmoi data.
Configuration
| Var | Default | What it does |
|---|---|---|
DF_NAME | (prompts) | Display name. Pre-seed to skip the chezmoi prompt on first run. |
DF_EMAIL | (prompts) | Email. Pre-seed to skip the chezmoi prompt on first run. |
DF_REPO | cadebrown/dotfiles | GitHub owner/repo slug used by curl-bootstrap. Override to fork. |
DF_PATH | (auto-detect) | Where the dotfiles repo lives on disk. Defaults to the script’s parent dir. |
DF_LINK | $HOME/dotfiles | Symlink in $HOME that points at DF_PATH. |
DF_DIRS | dev:bones:misc | Colon-separated list of subdirs created in $HOME by install/dirs.sh. |
Behavior toggles
| Var | Default | What it does |
|---|---|---|
DF_USE_PLAT | 0 | Per-PLAT directory isolation. 1 enables $LOCAL_PLAT=$HOME/.local/$PLAT; 0 collapses to $HOME/.local. Accepts 1|true|yes|on (case-insensitive). See PLAT isolation. |
DF_BREW_UPGRADE | 1 (macOS), 0 (Linux) | Whether to run brew upgrade and brew upgrade --cask --greedy. Auto-set to 1 in upgrade mode. |
DF_DEBUG | 0 | Set to 1 for verbose [dbug] output with timing info on every run_logged command. |
DF_FORCE | 0 | Used by install/plat-decommission.sh to skip the deletion confirmation prompt. |
DF_CARGO_STRATEGIES | (unset) | Override cargo binstall --strategies. E.g. compile to skip GitHub release fetchers (useful behind a VPN). |
Scratch space
| Var | Default | What it does |
|---|---|---|
DF_SCRATCH | (unset) | Path to scratch root. Setting this enables scratch mode (symlinks heavy $HOME dirs). |
DF_SCRATCH_LINK | $HOME/scratch | The $HOME symlink that points at scratch. Bootstrap creates this if DF_SCRATCH is set. |
DF_LINKS | ~/.local:~/.cache:~/.vscode:~/.vscode-server:~/.cursor:~/.cursor-server:~/.nv:~/.npm:~/.oh-my-zsh:~/.oh-my-zsh-custom | Colon-separated dirs to redirect to scratch. |
See Scratch space.
Skip flags
Each DF_DO_* flag defaults to 1 (run). Set to 0 to skip.
| Var | Step | Skips |
|---|---|---|
DF_DO_SCRATCH | 0 | Scratch space symlink setup (auto-0 in update/upgrade modes) |
DF_DO_DIRS | 0.1 | ~/dev, ~/bones, ~/misc creation |
DF_DO_PACKAGES | 4 | Homebrew + brew bundle |
DF_DO_MACOS_SERVICES | 5 | Colima service registration (macOS) |
DF_DO_MACOS_SETTINGS | 5.5 | Dock/Finder/keyboard/etc. defaults (macOS) |
DF_DO_MACOS_QUICK_ACTIONS | 5.6 | Finder Quick Actions install (macOS) |
DF_DO_ZSH | 3 | oh-my-zsh + plugins |
DF_DO_NODE | 6 | nvm + Node.js + global npm packages |
DF_DO_RUST | 6 | rustup + cargo tools |
DF_DO_PYTHON | 6 | uv + per-tool isolated venvs |
DF_DO_CLAUDE | 6 | Claude Code binary + plugins + MCP servers + overlay skills |
DF_DO_CODEX | 6 | Codex CLI binary + managed config + hooks |
DF_DO_CURSOR | 6 | Cursor settings symlinks + extensions |
DF_DO_VSCODE | 6 | VS Code extensions |
DF_DO_CMAKE | 6 | CMake toolchain file deployment |
DF_DO_LOCAL_LLM | 6.5 | Local LLM tooling (HuggingFace cache + binary checks) |
DF_DO_MEMORY | 6.6 | Agent memory stack (cass + qmd + ~/kb + daemons) |
DF_DO_BLENDER_MCP | 6.7 | Blender MCP addon install |
DF_DO_AUTH | 7 | Default 0. Set to 1 to run interactive token setup. |
DF_DO_OVERLAYS | 8 | Skip all overlay bootstrap scripts |
Internal (set by _lib.sh, not user-facing)
These are exported by _lib.sh for install scripts to consume — don’t override unless you know why.
| Var | Source | Value |
|---|---|---|
OS | _lib.sh | darwin or linux |
ARCH | _lib.sh | x86_64 or aarch64 (normalized) |
PLAT | _lib.sh | Detected platform name (e.g. plat_Darwin_arm64); empty if no spec matches |
LOCAL_PLAT | _lib.sh | Install root: $HOME/.local (flat) or $HOME/.local/$PLAT (PLAT-on) |
ARCH_BIN | _lib.sh | $LOCAL_PLAT/bin |
RUSTUP_HOME | _lib.sh | $LOCAL_PLAT/rustup |
CARGO_HOME | _lib.sh | $LOCAL_PLAT/cargo |
CARGO_TARGET_DIR | _lib.sh | $LOCAL_PLAT/cargo-build (workaround for macOS Sequoia ar/ld in /var/folders/) |
NVM_DIR | _lib.sh | $LOCAL_PLAT/nvm |
UV_TOOL_BIN_DIR | _lib.sh | $ARCH_BIN (where uv tool entrypoints land) |
UV_TOOL_DIR | _lib.sh | $LOCAL_PLAT/uv/tools (per-tool venvs) |
UV_PYTHON_INSTALL_DIR | _lib.sh | $LOCAL_PLAT/uv/python (uv-managed Python) |
CONAN_HOME | _lib.sh | $LOCAL_PLAT/conan2 |
DF_ROOT | _lib.sh | The dotfiles repo root (parent of install/) |
DF_PACKAGES | _lib.sh | $DF_ROOT/packages |
DF_OVERLAYS | _lib.sh | Bash array of discovered dotfiles-*/ overlay paths |
DF_INSTALL_DIR | bootstrap.sh | $DF_ROOT/install |
DF_MODE | bootstrap.sh | install, update, or upgrade |
GIT_CONFIG_GLOBAL | _lib.sh | Forced to /dev/null so install scripts aren’t affected by SSH-rewriting gitconfig |
Pre-seeding chezmoi
These get cached in ~/.config/chezmoi/chezmoi.toml on first init and don’t re-prompt:
| chezmoi data key | Source | Notes |
|---|---|---|
name | DF_NAME env or interactive prompt | Used in templates as {{ .name }} |
email | DF_EMAIL env or interactive prompt | Used in templates as {{ .email }} |
use_plat | DF_USE_PLAT env or false default | Used in templates as {{ .use_plat }} to gate PLAT-isolated paths |
Edit ~/.config/chezmoi/chezmoi.toml directly to change these without re-running chezmoi init.
Bootstrap flow
Step-by-step diagram of what bootstrap.sh actually does, with the DF_DO_* skip flag for each phase. Steps run in order — failures in any phase abort the rest (except VS Code/Cursor extension installs and a few other clearly-flagged log-warn-but-continue cases).
flowchart TD
A[curl bootstrap.sh] --> S0["0 scratch links<br/>DF_DO_SCRATCH"]
S0 --> S01["0.1 ~/dev ~/bones ~/misc<br/>DF_DO_DIRS"]
S01 --> S05["0.5 clone repo to ~/dotfiles"]
S05 --> S03["0.3 detect PLAT capability<br/>(always; tunes compiler flags)"]
S03 --> S1["1 install chezmoi binary<br/>(idempotent)"]
S1 --> S2["2 chezmoi init --apply --force<br/>(renders home/*.tmpl into ~/)"]
S2 --> S27["2.7 PATH sanity check<br/>(verifies ARCH_BIN writable, no broken symlinks)"]
S27 --> S3["3 oh-my-zsh + plugins<br/>DF_DO_ZSH"]
S3 --> S4["4 Homebrew + Brewfile<br/>DF_DO_PACKAGES"]
S4 -.macOS.-> S5["5 Colima service<br/>DF_DO_MACOS_SERVICES"]
S5 -.macOS.-> S55["5.5 defaults write<br/>DF_DO_MACOS_SETTINGS"]
S55 -.macOS.-> S56["5.6 Quick Actions<br/>DF_DO_MACOS_QUICK_ACTIONS"]
S4 --> S6
S56 --> S6
subgraph S6["6 language runtimes (each independent)"]
N["node.sh<br/>DF_DO_NODE"]
R["rust.sh<br/>DF_DO_RUST"]
P["python.sh<br/>DF_DO_PYTHON"]
C["claude.sh<br/>DF_DO_CLAUDE"]
X["codex.sh<br/>DF_DO_CODEX"]
V["cursor/vscode.sh<br/>DF_DO_CURSOR / DF_DO_VSCODE"]
K["cmake.sh<br/>DF_DO_CMAKE"]
end
S6 --> S65["6.5 local LLM<br/>DF_DO_LOCAL_LLM"]
S65 --> S66["6.6 agent memory stack<br/>DF_DO_MEMORY"]
S66 --> S67["6.7 blender-mcp addon<br/>DF_DO_BLENDER_MCP"]
S66 --> S7["7 auth.sh walk<br/>DF_DO_AUTH (default 0)"]
S7 --> S8["8 overlay bootstraps<br/>DF_DO_OVERLAYS"]
Step details
| Step | Script | What | Idempotent? |
|---|---|---|---|
| 0 | install/scratch.sh | Symlink heavy $HOME dirs to $DF_SCRATCH/.paths/. No-op if DF_SCRATCH unset. | Yes |
| 0.1 | install/dirs.sh | Create ~/dev, ~/bones, ~/misc (or $DF_DIRS). | Yes |
| 0.5 | inline | git clone if first run; git pull --ff-only in update/upgrade modes. | Yes |
| 0.3 | inline | Re-detect PLAT against the just-cloned repo’s install/plat/ dir. Upgrades the PLAT value if a higher CPU level matches now (e.g. v3 → v4). | Yes |
| 1 | install/chezmoi.sh | Download chezmoi to $ARCH_BIN/chezmoi. Skipped if file already executable. | Yes |
| 2 | (inline) | chezmoi init --apply --force --exclude=scripts. Renders home/*.tmpl into ~/. --exclude=scripts skips run_onchange_*.sh.tmpl (bootstrap calls install scripts directly). | Yes |
| 2.7 | inline | Sanity-check that $ARCH_BIN, $CARGO_HOME, $RUSTUP_HOME, $NVM_DIR parents exist and aren’t broken symlinks. Aborts if anything’s wrong. | Yes |
| 3 | install/zsh.sh | Clone or update oh-my-zsh + plugins. | Yes |
| 4 | install/homebrew.sh (macOS) or install/linux-packages.sh | Install Homebrew, run brew bundle install --file=Brewfile, optionally brew upgrade and brew upgrade --cask --greedy. | Yes |
| 5 | install/macos-services.sh | Register Colima as a launchd service; symlink Docker plugins. macOS only. | Yes |
| 5.5 | install/macos-settings.sh | defaults write for Dock, Finder, keyboard, trackpad, Safari, iTerm2, screen lock. Power management requires sudo (silently skipped if cache expired). | Yes |
| 5.6 | install/macos-quick-actions.sh | Deploy *.workflow bundles to ~/Library/Services/; flush pbs. | Yes |
| 6 | various | See language-runtime table below. Each script is independent; failures cascade only via die (not log_warn). | Yes |
| 6.5 | install/local-llm.sh + install/opencode.sh | Create $LOCAL_PLAT/.cache/huggingface; verify ollama/mlx-lm/mlx-openai-server/opencode binaries. | Yes |
| 6.6 | install/memory.sh | Agent memory stack: cass binary (checksum-verified GitHub release) + session-history index, ~/kb knowledge repo, qmd collections/embeddings, memory daemons. | Yes |
| 6.7 | install/blender-mcp.sh | Download addon.py into Blender’s user addons; enable headlessly. Skipped if Blender not installed. | Yes |
| 7 | install/auth.sh | Walk every service, prompt [k]eep / [u]pdate / [d]elete per service. Default off — set DF_DO_AUTH=1 to enable. | Yes |
| 8 | overlay scripts | Run bash $DF_ROOT/dotfiles-*/bootstrap.sh "$DF_MODE" for each overlay. | Per overlay |
Step 6 in detail
| Sub-step | Script | What | Notes |
|---|---|---|---|
| 6a | install/node.sh | Install nvm to $NVM_DIR; install/upgrade Node v25; install npm.txt packages globally. | Lazy-loaded in interactive shells. |
| 6b | install/rust.sh | Install rustup (Homebrew on macOS, sh.rustup.rs on Linux); install/update stable toolchain; cargo binstall every entry in cargo.txt. | rustup self-update is --no-self-update except in upgrade mode. |
| 6c | install/python.sh | Install uv to $ARCH_BIN; uv tool install every entry in pip.txt (each gets isolated venv). | # macos-only and # python=X.Y markers parsed from comments. |
| 6d | install/claude.sh | Download Claude Code binary to $ARCH_BIN/claude if version differs; install plugins; register MCP servers (with auth=gh resolution); deploy overlay skills. | Atomic write via temp file + mv -f. |
| 6e | install/codex.sh upgrade | Sync managed Codex config (config.toml, hooks.json, df-chezmoi-guard) + healthcheck (binary comes from the pinned @openai/codex in npm.txt). | Healthcheck dies on config drift. |
| 6f | install/cursor.sh | Symlink Cursor settings; install extensions from cursor-extensions.txt. | Extension failures are warnings, not fatal. |
| 6g | install/vscode.sh | Install extensions from vscode-extensions.txt. | Same warning-not-fatal pattern. |
| 6h | install/cmake.sh | Copy toolchain files (llvm-21/22.cmake, gcc-13/15.cmake, _brew.cmake) from repo to $LOCAL_PLAT/cmake/toolchains/. | Always overwrites — keeps deployed copies in sync with repo. |
Modes
| Mode | What changes |
|---|---|
install (default) | Full idempotent setup. DF_DO_SCRATCH=1 (run scratch step). |
update | Same steps, but: git pull --ff-only in step 0.5, DF_DO_SCRATCH=0 (assume scratch is already set up), tools self-update where they support it. |
upgrade | Same as update, plus: DF_BREW_UPGRADE=1 (greedy cask refresh), rustup self-update, nvm install 25 --reinstall-packages-from=25 --latest-npm, npm install -g <pkg>@latest per package, uv self update + uv tool upgrade --all, oh-my-zsh git pull, VS Code/Cursor extension --force reinstall. Claude Code and Codex CLI always re-download to latest regardless of mode. |
Reading the source
The canonical source is bootstrap.sh itself — header comment block has the full flag table, then numbered ### N. ### step markers. To trace what a single step actually does, jump to install/<step>.sh. Each install script sources _lib.sh for path variables and logging helpers.
Docs and hosting
The documentation site at dotfiles.cade.io is built with mdBook and deployed automatically on every push to main.
How it works
push to main
→ Cloudflare Pages detects the push
→ runs infra/cloudflare/build.sh
→ installs cargo-binstall + mdbook
→ runs `mdbook build docs`
→ deploys docs/book/ to dotfiles.cade.io
The entire pipeline is defined in two files:
infra/cloudflare/main.tf– OpenTofu config that creates the Cloudflare Pages project, binds the custom domain (dotfiles.cade.io), and sets up the CNAME DNS recordinfra/cloudflare/build.sh– build script that runs inside Cloudflare’s build environment (installs mdbook via cargo-binstall, then builds)
Local development
mdbook serve docs/ --open # live reload at localhost:3000
Changes to any .md file under docs/ are reflected instantly in the browser.
Doc structure
docs/
├── book.toml # mdBook config (title, theme, repo link)
├── SUMMARY.md # Table of contents / sidebar nav
├── intro.md # Homepage
├── setup/
│ ├── bootstrap.md # Bootstrap instructions per platform
│ ├── chezmoi.md # Dotfile management with chezmoi
│ └── packages.md # Package layers (cargo, npm, pip, brew)
├── usage/
│ ├── updates.md # Day-to-day workflow
│ └── troubleshooting.md
└── infra/
└── docs-and-hosting.md # This page
Infrastructure management
The Cloudflare Pages project is managed with OpenTofu (open-source Terraform):
cd infra/cloudflare
export CLOUDFLARE_API_TOKEN=...
tofu plan # preview changes
tofu apply # create/update Pages project + DNS
terraform.tfvars holds account_id and github_owner – gitignored, copy from terraform.tfvars.example on each machine.
What OpenTofu creates
| Resource | Purpose |
|---|---|
cloudflare_pages_project | Pages project linked to GitHub, runs build.sh on push |
cloudflare_pages_domain | Binds dotfiles.cade.io to the project |
cloudflare_record | CNAME dotfiles → <project>.pages.dev (proxied) |
This same pattern (OpenTofu + Cloudflare Pages + mdBook) is used across other projects at cade.io.