cade’s dotfiles
Personal dotfiles for macOS and Linux. One command bootstraps a complete dev environment – idempotent, safe on shared NFS home directories across CPU architectures. No sudo required on Linux.
curl -fsSL https://raw.githubusercontent.com/cadebrown/dotfiles/main/bootstrap.sh | bash
Prompts for name and email once. Re-run anytime to converge.
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 per-CPU 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 + venv | $LOCAL_PLAT/venv/ | packages/pip.txt |
Rust tools are installed via cargo-binstall which downloads pre-built binaries from GitHub releases when available, falling back to source compilation. On macOS, rustup comes from Homebrew (code-signed, required on Sequoia+ where the linker enforces provenance).
AI tools
- Claude Code – native binary downloaded from Anthropic’s release bucket, plus plugins from
packages/claude-plugins.txtand MCP servers frompackages/claude-mcp.txt - Codex CLI – native binary from GitHub releases
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)
Auth (opt-in)
install/auth.sh is an interactive helper that creates ~/.{service}.env files (chmod 600) for GITHUB_TOKEN, ANTHROPIC_API_KEY, and OPENAI_API_KEY. These are sourced automatically by all install scripts. 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.
PLAT isolation
Every compiled binary lives under ~/.local/$PLAT/ where PLAT is detected from CPU features at shell startup. On a shared NFS home, each machine installs into its own PLAT directory – binaries are isolated, text configs are shared freely.
~/.local/plat_Linux_x86-64-v4/ # AVX-512 (Ice Lake+, Zen 4+)
~/.local/plat_Linux_x86-64-v3/ # AVX2 (Haswell+, Zen 2+)
~/.local/plat_Linux_x86-64-v2/ # SSE4.2 (Nehalem+)
~/.local/plat_Linux_aarch64/ # ARM Linux (Graviton, Ampere)
~/.local/plat_Darwin_arm64/ # Apple Silicon
~/.local/plat_Darwin_x86-64/ # Intel Mac
The shell profile detects the current machine’s PLAT and puts only that directory’s paths on PATH. One home directory, many machines, no conflicts.
Each PLAT also gets CPU-specific compiler flags (-march=x86-64-v3, etc.) via .plat_env.sh scripts, so tools compiled from source use the best available instruction set.
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
| 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 |
| Day-to-day workflow | Updating, adding packages, editing dotfiles |
| Troubleshooting | Tools not found, PATH issues, build failures |
| Docs and hosting | How this site is built, deployed, and managed |
Bootstrap a new machine
One-liner
curl -fsSL https://raw.githubusercontent.com/cadebrown/dotfiles/main/bootstrap.sh | bash
Prompts for name and email once. Everything else runs unattended.
Skip the prompts
Pre-seed name and email to run fully unattended:
DF_NAME="Your Name" DF_EMAIL="you@example.com" \
curl -fsSL https://raw.githubusercontent.com/cadebrown/dotfiles/main/bootstrap.sh | bash
Values are cached in ~/.config/chezmoi/chezmoi.toml. On re-runs, they’re read from the cache — no prompts.
From a local clone
git clone https://github.com/cadebrown/dotfiles ~/dotfiles
DF_NAME="Your Name" DF_EMAIL="you@example.com" ~/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
- chezmoi →
~/.local/$PLAT/bin/chezmoi - Dotfiles applied via
chezmoi apply- Shell configs for both zsh (
.zprofile) and bash (.bash_profile) - Both shells do identical PLAT 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/- Uses Homebrew’s
rustup(code-signed), required on macOS Sequoia+ where the linker enforcescom.apple.provenanceon object files cargo-binstalldownloads pre-built binaries from GitHub releases when available, falls back to source compilation otherwise- Cargo tools install to
~/.local/$PLAT/cargo/bin/
- Uses Homebrew’s
- Python via uv →
~/.local/$PLAT/venv/ - Claude Code native binary →
~/.local/$PLAT/bin/claude+ plugins + MCP servers - Codex CLI native binary →
~/.local/$PLAT/bin/codex - CMake toolchain files →
~/.local/$PLAT/cmake/toolchains/llvm.cmakeandgcc.cmakedeployed frominstall/cmake/toolchains/~/.profilesetsCMAKE_TOOLCHAIN_FILEto the LLVM file automatically
- Local LLM tooling (step 6.5) — HuggingFace cache dir + Ollama context-boosted model aliases
- Creates
$LOCAL_PLAT/.cache/huggingfacefor mlx-lm model weights - Context-boosted aliases (e.g.
qwen3-coder:30b-ctx256k) created if the base model is pulled - Skipped gracefully if Ollama is not installed
- Creates
Total time: ~5 minutes on a fast connection (most packages pour as precompiled bottles).
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
- chezmoi →
~/.local/$PLAT/bin/chezmoi - Dotfiles applied via
chezmoi apply- Shell configs for both zsh (
.zprofile) and bash (.bash_profile) - Both shells do identical PLAT detection and PATH setup
- 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, independent of the host system glibc
- Most packages pour as precompiled bottles; glibc builds from source (~2 min) on first run
- Custom Python@3.14 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 compilation otherwise
- Python via uv →
~/.local/$PLAT/venv/ - Claude Code native binary →
~/.local/$PLAT/bin/claude+ plugins + MCP servers - Codex CLI native binary →
~/.local/$PLAT/bin/codex - CMake toolchain files →
~/.local/$PLAT/cmake/toolchains/llvm.cmakeandgcc.cmakedeployed frominstall/cmake/toolchains/~/.profilesetsCMAKE_TOOLCHAIN_FILEto the LLVM file automatically
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_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 + venv
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 + Ollama context aliases)
DF_DO_AUTH=1 # run interactive API token setup
DF_DO_OVERLAYS=0 # skip all overlay bootstraps (dotfiles-*/bootstrap.sh)
DF_BREW_UPGRADE=0 # skip Homebrew upgrades (macOS default: 1, Linux default: 0)
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)
Run bootstrap.sh on each machine independently:
- chezmoi reads the cached config — no re-prompting
- Dotfiles are already applied — no changes
- Each machine detects its CPU level and 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.
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="you@example.com" \
~/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)
Set up API tokens interactively:
bash ~/dotfiles/install/auth.sh
# Or during bootstrap:
DF_DO_AUTH=1 ~/dotfiles/bootstrap.sh
Guides you through setting up GITHUB_TOKEN, ANTHROPIC_API_KEY, and OPENAI_API_KEY. Creates ~/.{service}.env files (chmod 600) that are sourced automatically by all install scripts.
Managing dotfiles
chezmoi manages the files in home/ and applies them to ~/, resolving templates along the way.
The quick version
chezmoi edit ~/.zshrc # edit a dotfile (opens in $EDITOR, applies on save)
chezmoi 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)
{{ .chezmoi.os }} "darwin" or "linux"
{{ .chezmoi.arch }} "amd64" or "arm64"
{{ .chezmoi.username }} system login name (auto-detected)
{{ .chezmoi.homeDir }} home directory path
Example — Linux-only alias:
{{ if eq .chezmoi.os "linux" -}}
alias open='xdg-open'
{{ end -}}
Editing dotfiles
Via chezmoi (recommended — auto-applies on save):
chezmoi edit ~/.zshrc
chezmoi edit ~/.zprofile # 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.md,~/.codex/skills/, and~/.codex/rules/are intentionally Codex-specific and do not mirror Claude one-for-one
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 |
| Claude plugins | packages/claude-plugins.txt | install/claude.sh | All |
| Claude MCP servers | packages/claude-mcp.txt | install/claude.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
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
@cometix/ccline
Re-run: bash ~/dotfiles/install/node.sh
Currently ships @cometix/ccline — a Rust-based status line for Claude Code with themes and TUI config (ccline --config).
3. pip — Python packages
# packages/pip.txt
requests
black
numpy
some-macos-tool # macos-only (requires Metal / only available on macOS)
aider-chat # python=3.12 (scipy has no wheels for python 3.14+)
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.aider-chatneeds 3.12 becausescipyhas no wheels for Python 3.14+)
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 extensions
# Add to packages/vscode-extensions.txt
ms-python.python
charliermarsh.ruff
Re-run: bash ~/dotfiles/install/vscode.sh
To capture newly installed extensions back into the file (union — never removes):
bash ~/dotfiles/install/vscode.sh sync-extensions
Note:
settings.jsonis not tracked — it contains an embedded GitLab token (cmake.configureEnvironment). Extensions only.
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 |
aider-chat | packages/pip.txt (# python=3.12) | CLI coding agent; pinned to Python 3.12 because scipy has no wheels for 3.14+ |
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 creates context-boosted Ollama
model aliases so the 4096-token default doesn’t starve the agentic tool-use loop.
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.
Python@3.14 patches: On Linux, install/patch-homebrew-python.sh automatically patches
the python@3.14 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)
When Homebrew LLVM is present, ~/.profile automatically sets:
export CMAKE_TOOLCHAIN_FILE="$_LOCAL_PLAT/cmake/toolchains/llvm.cmake"
The toolchain configures:
| CMake variable | Value |
|---|---|
CMAKE_C_COMPILER | $LOCAL_PLAT/brew/opt/llvm/bin/clang |
CMAKE_CXX_COMPILER | $LOCAL_PLAT/brew/opt/llvm/bin/clang++ |
CMAKE_AR / RANLIB / NM | llvm-ar, llvm-ranlib, llvm-nm |
CMAKE_LINKER_TYPE | LLD (Linux only; macOS requires Apple ld) |
CMAKE_CUDA_HOST_COMPILER | clang++ |
Switching to GCC 15
Per-invocation:
CMAKE_TOOLCHAIN_FILE="$_LOCAL_PLAT/cmake/toolchains/gcc.cmake" cmake -B build
Per-session:
export CMAKE_TOOLCHAIN_FILE="$_LOCAL_PLAT/cmake/toolchains/gcc.cmake"
Per-project (CMakePresets.json):
{ "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "/absolute/path/to/gcc.cmake" } }
The GCC toolchain uses versioned binaries (gcc-15, g++-15, etc.) because Homebrew
does not create unversioned gcc symlinks on macOS. Linker priority: 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.
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 powered by CCometixLine (@cometix/ccline in npm.txt). To pick a theme: ccline --config.
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/default.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_codex/skills/ in the repo and apply to ~/.codex/skills/.
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 --profile theme_neon
codex --profile theme_sunrise
codex --profile theme_mint
codex execpolicy check --pretty --rules ~/.codex/rules/default.rules -- git status
codex '$project-bootstrapper Map this repository and propose the first validation step.'
codex '$simulation-lab Define state variables and a minimal validation case for this model.'
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.
Overview
Two inference backends, one coding agent layer:
| Tool | Format | Server | When to use |
|---|---|---|---|
| Ollama | GGUF | localhost:11434/v1 (OpenAI-compatible) | Primary backend — always-on LaunchAgent, large models, multi-client |
| mlx-lm | MLX | localhost:8080/v1 (OpenAI-compatible) | On-demand — faster on Apple Silicon Metal, but tool calling broken (PR #1027) |
| OpenCode | — | TUI client | Primary coding agent (full agentic loop with tools) |
| aider | — | CLI client | Quick edits, one-shot diffs, git integration |
Ollama and mlx-lm use different model formats (GGUF vs MLX) and store files separately. They cannot share downloads.
Quick start
Pull a model and run OpenCode:
# Pull a model (GGUF, stored at ~/.ollama/models)
ollama pull qwen3-coder:30b
# Start OpenCode (auto-connects to Ollama at localhost:11434)
opencode
# Or use aider
aider
Ollama
Installed via Homebrew (brew "ollama" in packages/Brewfile). On macOS, managed as a
LaunchAgent — starts at login, accessible at http://127.0.0.1:11434.
# Check running models
ollama list
# Pull a model
ollama pull llama3.3:70b
# Run a quick test
ollama run qwen3-coder:30b "hello"
# API endpoint (OpenAI-compatible)
curl http://localhost:11434/v1/models
Context windows
Ollama’s default context window (4096 tokens) is too small for agentic tool-use loops — the system prompt + tool schemas + conversation history fill the window immediately.
install/opencode.sh creates context-boosted model aliases automatically:
| Alias | Base model | Context | Memory (weights + KV) |
|---|---|---|---|
qwen3-coder:30b-ctx256k | qwen3-coder:30b | 256K | ~78 GB |
llama3.3:70b-ctx128k | llama3.3:70b | 128K | ~83 GB |
gpt-oss:20b-ctx128k | gpt-oss:20b | 128K | ~39 GB |
qwen2.5-coder:7b-ctx128k | qwen2.5-coder:7b | 128K | ~20 GB |
These fit comfortably on an M3 Max 128 GB (unified memory — no CPU/GPU split, Metal accesses all of it).
gpt-oss:120b is excluded — confirmed Ollama hang bug with large num_ctx for that model.
To recreate aliases after pulling new models:
bash ~/dotfiles/install/opencode.sh
Model storage
Ollama stores models at ~/.ollama/models (managed by the Ollama app — not redirected by dotfiles).
On a shared NFS home, point it at scratch if needed:
OLLAMA_MODELS=/scratch/$USER/ollama/models ollama pull qwen3-coder:30b
mlx-lm
Installed via pip (mlx-lm in packages/pip.txt, tagged # macos-only). Apple Silicon only —
runs on Metal, skips CPU. Skipped automatically on Linux by install/python.sh.
Not started automatically — launch on demand.
# Start the server on localhost:8080
mlx_lm.server --model mlx-community/Qwen3-30B-A3B-4bit --port 8080
# Models are stored at $HF_HOME (~/.local/$PLAT/.cache/huggingface)
Note: Tool calling in
mlx_lm.serveris currently broken upstream (draft fix in PR #1027). Until that merges, use Ollama for agentic workflows. mlx-lm is useful for fast one-shot generation.
HF_HOME is set by .zprofile to $_LOCAL_PLAT/.cache/huggingface — model weights go to scratch
if scratch is configured, never polluting NFS home quotas.
OpenCode
Installed via Homebrew (brew "opencode" in packages/Brewfile). TUI coding agent that runs a
full agentic loop with file read/write/edit tools. Config at ~/.config/opencode/opencode.json
(deployed by chezmoi).
opencode # launch in current directory
opencode --help # options
The default model is qwen3-coder:30b-ctx256k (Ollama). Switch models inside the TUI with /model.
OpenCode does not auto-detect OLLAMA_HOST — the provider is configured explicitly in
opencode.json with baseURL: "http://127.0.0.1:11434/v1".
To add a new model to the OpenCode model list, edit home/dot_config/opencode/opencode.json
and run chezmoi apply. If the model needs a context-boosted alias, add it to
install/opencode.sh and re-run it.
aider
Installed via pip (aider-chat in packages/pip.txt, tagged # python=3.12 because scipy has
no wheels for Python 3.14+). Config at ~/.aider.conf.yml (deployed by chezmoi as a template):
- macOS: defaults to
ollama/qwen3-coder:30b-ctx256k(local inference) - Linux: empty config — falls through to
ANTHROPIC_API_KEYor an explicit--modelflag
aider # use default model from ~/.aider.conf.yml
aider --model ollama/llama3.3:70b-ctx128k # override model
aider --model anthropic/claude-opus-4 # use Anthropic API (needs ANTHROPIC_API_KEY)
aider has git integration built in — it commits changes automatically with descriptive messages.
run_onchange hooks
chezmoi re-runs the relevant install scripts automatically when tracked files change:
| Trigger file | Script re-run |
|---|---|
packages/pip.txt | install/local-llm.sh (verifies mlx-lm/aider binaries) |
home/dot_config/opencode/opencode.json | install/opencode.sh (recreates context aliases) |
This means chezmoi update after pulling dotfile changes will re-verify the local LLM setup
and recreate any missing model aliases.
Troubleshooting
Quick reference for when things go wrong. Check here before digging into scripts.
Tool not found after bootstrap
echo "$_PLAT" # confirm which platform the shell resolved
ls ~/.local/$_PLAT/bin/ # chezmoi, uv, claude should be here
ls ~/.local/$_PLAT/cargo/bin/ # fd, sd, zoxide, etc.
which fd # should point into ~/.local/$_PLAT/
If $_PLAT is empty or wrong, .zprofile wasn’t sourced. Open a new login shell (zsh -l) or source it:
source ~/.zprofile
nvm or node not available in a script
nvm.sh is lazy-loaded in interactive shells only. Non-interactive shells get node/npm via the PATH entry .zprofile/.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="you@example.com" 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/venv/bin Python venv
~/.local/$PLAT/cargo/bin Rust tools (fd, sd, zoxide, etc.)
~/.local/$PLAT/nvm/.../bin Node.js
~/.local/$PLAT/bin chezmoi, uv, claude (Linux)
~/.local/bin arch-neutral scripts
/opt/homebrew/bin Homebrew (macOS)
/usr/bin system
Diagnose with:
which <tool> # where it's resolving from
type -a <tool> # all locations on PATH
echo $PATH | tr ':' '\n' # full PATH in order
If a Homebrew tool is shadowing a cargo tool, check packages/cargo.txt and packages/Brewfile for duplicates — remove the one you don’t want.
Cloudflare Pages build failing
Check the build log via the API:
ACCOUNT="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)
This is fixed in current versions. Both .zprofile (zsh) and .bash_profile (bash) now resolve ~/.local symlinks before setting _LOCAL_PLAT, ensuring all PATH entries use consistent physical paths.
If you installed before this fix was added:
# Apply updated shell profiles
chezmoi apply ~/.zprofile ~/.bash_profile
# Open a new shell
exec zsh -l # or: exec bash -l
# Verify only one PLAT appears
echo "$_PLAT" # should show only the detected PLAT
echo "$PATH" | tr ':' '\n' | grep plat # all paths should have the same PLAT prefix
On a shared NFS home with scratch space, ~/.local is a symlink to /scratch/$USER/.paths/.local. The shell profiles now resolve this symlink so Homebrew, cargo, nvm, and other tools all add the same physical path to PATH (no duplicates).
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
Python@3.14 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 python@3.14
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
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.