Skip to content

Latest commit

 

History

404 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dotfiles

These are my personal macOS dotfiles, managed with chezmoi. Highly opinionated and tuned for my workflow on Apple Silicon — feel free to fork, adapt, and steal patterns. Don't blindly apply this to your machine.

Warning

This repo writes to ~, modifies macOS system defaults, installs ~250 Homebrew packages, and clones external repositories. Read the code before running it. If you want to use any of this, fork first and adapt to your needs.


What's inside

  • Shell: zsh with antidote, starship, lazy-loaded language tooling
  • Prompt drift indicator: a zsh precmd hook runs a split chezmoi verify in the background and publishes glyphs to ~/.cache/chezmoi-drift/state, rendered by starship. ~ live drift · * uncommitted source · / ahead/behind · ? no successful fetch in 24h. See Guard system.
  • Agent guards: chezmoi-guard hooks for Claude Code, Codex, and opencode hard-block agents editing managed live files and block Stop until chezmoi source changes are committed and pushed.
  • Commit driver: chezmoi-commit (alias cmc) splits pending source changes into logical commits, writes each message with a cheap model, pushes.
  • Editor: neovim (managed as a separate repo via .chezmoiexternal.toml) + ideavim
  • Multiplexers: tmux (tpm-managed plugins) and zellij
  • Terminal: WezTerm primarily, iTerm2 fallback
  • Window management (macOS): yabai + skhd + sketchybar
  • Secrets: age for whole-file encryption, 1Password op:// templates for rotatable tokens
  • Git: delta pager, SSH commit signing via 1Password agent
  • Bootstrap: chezmoi init --apply plus a handful of run_once_after_* scripts
  • Brewfile: ~400 entries spanning taps, brews, casks, vscode extensions, cargo, and uv. For the live breakdown: awk '/^(tap|brew|cask|vscode|cargo|uv) /{print $1}' Brewfile.tmpl | sort | uniq -c

Quick start

Option 1: dry-run first (recommended for tire-kickers)

Look without touching:

chezmoi init --apply=false https://github.com/mackhaymond/dotfiles.git
chezmoi diff

This clones the source dir to ~/.local/share/chezmoi/ so you can browse, runs the init prompts, and shows what would change without writing anything.

Option 2: full bootstrap (you've reviewed and want to apply)

Prerequisites: macOS, Homebrew installed, a 1Password account with the desktop app's CLI integration enabled (Settings → Developer → "Integrate with 1Password CLI"), and a personal age key (see Secrets).

brew install chezmoi age gh
brew install --cask 1password-cli   # if not already installed

# 1. Authenticate to clone over HTTPS
gh auth login

# 2. Place your age private key (replace item name with your own backup)
mkdir -p ~/.config/chezmoi
op read "op://Developer/chezmoi-age-key-mackbook/notesPlain" \
    > ~/.config/chezmoi/key.txt
chmod 600 ~/.config/chezmoi/key.txt

# 3. Apply
chezmoi init --apply https://github.com/mackhaymond/dotfiles.git

chezmoi init will prompt for: name, email, GitHub username. Answers are saved to ~/.config/chezmoi/chezmoi.toml and reused. The git commit signing key is pulled from 1Password automatically (no prompt) — see the Secrets section.

--apply will:

  • Render all templates and copy files into ~
  • Decrypt age files with your local key
  • Pull op:// values from 1Password at apply time
  • Clone external repos (mackhaymond/nvim, tmux-plugins/tpm)
  • Run brew bundle install against the templated Brewfile
  • Run idempotent setup scripts: macOS defaults, tpm plugins, broot launcher, opam user-setup

OAuth flows that can't be scripted (re-auth per machine):

gh auth login    # already done above; re-run as needed
aws sso login    # AWS SSO if you use it

Option 3: clone the source first, then apply

git clone https://github.com/mackhaymond/dotfiles.git ~/Code/dotfiles
chezmoi init --source=~/Code/dotfiles
# review ~/.local/share/chezmoi/ — actually it's symlinked from ~/Code/dotfiles
chezmoi diff
chezmoi apply

Selective install

This repo is intentionally monolithic — no init-time prompts asking "do you want X?" The cost of maintaining feature flags isn't justified for a single-author repo, and chezmoi already gives you three good ways to install only what you want:

Path-based — install specific files or trees

chezmoi init https://github.com/mackhaymond/dotfiles.git    # init WITHOUT --apply
chezmoi diff                                               # browse what's available

# Pick specific things:
chezmoi apply ~/.zshrc ~/.zprofile ~/.gitconfig            # just shell + git
chezmoi apply ~/.config/tmux                               # the whole tmux tree
chezmoi apply ~/.config/nvim ~/.config/wezterm             # editor + terminal

Type-based — install only certain entry types

Chezmoi categorizes everything into types: files, dirs, scripts, externals, encrypted, templates. Filter with --include / --exclude:

# All dotfiles, but skip the bootstrap automation (no Brewfile install,
# no macOS defaults script, no broot/tpm/opam setup)
chezmoi apply --exclude=scripts

# Files only - no scripts, no external repos cloned
chezmoi apply --exclude=scripts,externals

# Just decrypt and write the age-encrypted files
chezmoi apply --include=encrypted

# Useful for a server install: skip GUI tools, big package install,
# external repo clones
chezmoi apply --exclude=scripts,externals

Fork-and-delete — permanent exclusion

For a fork that doesn't want certain tools, the lowest-friction approach is to delete what you don't want before the first apply:

git clone https://github.com/mackhaymond/dotfiles.git ~/Code/dotfiles
cd ~/Code/dotfiles

# Drop macOS-only window-management bundle:
rm -rf dot_config/yabai dot_config/skhd dot_config/sketchybar dot_config/karabiner

# Drop the AI tooling configs:
rm -rf dot_config/opencode dot_agents

# Drop the Brewfile install entirely:
rm Brewfile.tmpl .chezmoiscripts/run_onchange_after_install-brew-packages.sh.tmpl

# Then init from your local source:
chezmoi init --source=~/Code/dotfiles --apply

This is the mathiasbynens/dotfiles philosophy — fork it, review it, customize it. No prompts to memorize, no flags to keep in sync with reality.

What you might want to skip in practice

The components most likely to be unwanted on a non-primary machine:

Component How to skip
Brewfile install (250 packages) --exclude=scripts OR delete Brewfile.tmpl + the brew-bundle script
macOS window management (yabai/skhd/sketchybar/karabiner) Auto-skipped on non-darwin; on darwin delete the dot_config/{yabai,skhd,sketchybar,karabiner}/ trees
macOS defaults script (modifies system settings) --exclude=scripts OR delete .chezmoiscripts/run_once_after_set-macos-defaults.sh.tmpl
External repo clones (nvim, tpm) --exclude=externals OR remove entries from .chezmoiexternal.toml
AI tooling configs Delete dot_config/opencode/, dot_agents/, dot_config/cursor/

Architecture

The repo is organized around chezmoi's source-state conventions:

Source filename Becomes in $HOME
dot_zshrc.tmpl .zshrc (Go-templated render)
private_dot_gitconfig.tmpl .gitconfig mode 0600, templated
executable_some-script.sh some-script.sh with +x
encrypted_*_dot_*.age age-decrypted at apply time
Brewfile.tmpl Brewfile (templated)
.chezmoiscripts/run_once_after_*.sh.tmpl executed once per machine, never deployed
.chezmoiscripts/run_onchange_*.sh.tmpl re-executed when rendered content changes

Stacking order (when multiple prefixes apply): encrypted_private_readonly_empty_executable_dot_.

Templates reference data variables computed at init time:

Variable Source
.homebrew_prefix computed from os+arch (darwin/arm64/opt/homebrew, darwin/amd64/usr/local, linux/home/linuxbrew/.linuxbrew)
.email, .name, .github_username prompted on first init via promptStringOnce
.chezmoi.os, .chezmoi.arch, .chezmoi.homeDir, .chezmoi.hostname auto-populated by chezmoi

.chezmoiignore uses Go-template conditionals so macOS-only configs (yabai, skhd, sketchybar, karabiner, raycast) get filtered out on Linux:

{{ if ne .chezmoi.os "darwin" -}}
.config/yabai/**
.config/skhd/**
...
{{ end -}}

Preview the rendered ignore list with chezmoi execute-template < .chezmoiignore.


Secrets

Hybrid approach with two patterns:

age for static, multi-secret files where rotation is rare:

  • ~/.zshenv.private — env exports (sourced by .zshenv)
  • ~/.ssh/config — private hostnames, internal IPs, agent socket paths

1Password op:// templates for rotatable single-value tokens:

File 1Password reference
~/.tmate.conf op://Developer/tmate-api-key/credential
~/.config/raycast/config.json op://Developer/raycast-access-token/credential
~/.config/sketchybar/plugins/todos.sh op://Developer/todoist-api-token/credential
~/.gitconfig (just the signingkey field) op://Personal/Github SSH Key/public_key

Templates contain only {{ onepasswordRead "..." }} placeholders — never the literal token. Rotation is "edit in 1Password → chezmoi apply."

The age private key is backed up in 1Password as a Secure Note. Lose the key without the backup and every age-encrypted file becomes unrecoverable.

SSH keys are managed by 1Password's SSH agent; the public side is in .gitconfig for commit signing. No SSH private keys are tracked.


Customization

If you want to use this as a starting point, fork first. Then:

  1. Update the bootstrap one-liner in this README to point at your fork.
  2. Rename or delete .chezmoi.toml.tmpl prompts you don't need.
  3. Rotate the age key — generate your own (age-keygen) and replace the recipient in .chezmoi.toml.tmpl. Re-encrypt each .age file with your new key.
  4. Replace the op:// references with your own 1Password vault structure.
  5. Trim the Brewfile — most of these packages are mine, not yours. Run brew bundle dump --describe --force --file=Brewfile.tmpl after pruning, or strip what you don't want.
  6. Strip macOS-only configs if you're on Linux (delete the dot_config/{yabai,skhd,sketchybar,karabiner,raycast}/ trees and the matching .chezmoiignore darwin block).
  7. Replace .chezmoiexternal.toml URLs with your own nvim repo (or delete the entry entirely if you vendor your nvim config).
  8. Edit .chezmoiscripts/run_once_after_set-macos-defaults.sh.tmpl — these reflect my preferences, not yours.

For machine-specific settings that don't need to live in git, put them in ~/.config/chezmoi/chezmoi.toml directly:

[data]
work_machine = true
extra_path = "/some/local/path"

These are then accessible as {{ .work_machine }} in any template.


Cross-platform

The repo is templated for cross-platform use, but only macOS is exercised today. To add a Linux machine:

  1. Run the bootstrap. homebrew_prefix resolves to /home/linuxbrew/.linuxbrew.
  2. macOS-only configs filter out automatically via .chezmoiignore.
  3. Brewfile.tmpl renders empty on Linux. Add an {{ else if eq .chezmoi.os "linux" }} block or a separate apt/dnf install script.
  4. Add Linux-only configs (.config/i3/, .Xresources, etc.) and gate them in .chezmoiignore.

Shell, git, editor, and most CLI configs already work on Linux because they use {{ .homebrew_prefix }} and {{ .chezmoi.homeDir }} instead of literal paths.


Guard system

The contract this repo enforces: never edit a managed live file; edit the source, apply, then commit with cmc. Four pieces hold that line. Removing any of them just removes a safety net — nothing else depends on them.

Prompt drift indicator (dot_zshrc.tmpl, "Tier C"). A precmd hook checks ~/.cache/chezmoi-drift/state with a no-fork $(<file) read and exports its glyphs; a background job refreshes that file with a split chezmoi verify (fast plain files every cycle, secrets/templates less often) plus git status/ahead-behind on the source repo, and publishes atomically. The glyphs: ~ live drift (the next chezmoi apply will clobber a hand edit), * uncommitted source, / ahead/behind origin, ? no fetch in 24h. .zshrc exports one env var per class (STARSHIP_CHEZMOI_DRIFT_{LIVE,UNCOMMITTED,SYNC,STALE}) and dot_config/starship.toml styles each: ~ bold red with a , * yellow, / blue, ? dim — so the everyday "you have uncommitted source changes" never looks like the "your hand edit is about to be clobbered" alarm. The nu tab deliberately hides these vars (dot_config/nushell/env.nu.tmpl) rather than show a value frozen at launch.

Agent guards (dot_claude/hooks/chezmoi-guard.ts, dot_codex/hooks/chezmoi-guard.ts, dot_config/opencode/plugins/chezmoi-guard.ts). One guard per agent runtime, same rules: edit-class tools targeting a chezmoi-managed live path are hard-denied (exact match against chezmoi managed), shell commands are denied when they carry write intent at a managed path (>, tee, cp, mv, sed -i, perl -i, rm, …; plain reads are allowed), and destructive git aimed at the source repo is denied (reset, rebase, merge, force-push, restore, checkout -- <path>, clean -f, stash push/pop/drop, commit --amend). Plain add/commit/push/status/diff are allowed. Each guard also remembers source files the session wrote and, at Stop, blocks once with a "commit and push" continuation (capped, loop-guarded). Everything fails open: a broken bun or chezmoi disables the guard rather than wedging the agent. Per-runtime details: dot_claude/hooks/README.md, dot_codex/hooks/README.md.

Commit driver (dot_local/bin/executable_chezmoi-commit, alias cmc). Scans the pending changes with gitleaks first (the same gitleaks git --staged scan the pre-commit hook runs, on a throwaway index) and aborts before anything is sent to a model if it finds a secret. Then reads git status in the source repo, asks a cheap model (claude haiku, codex fallback) to group the changed paths into logical commits with messages, validates the grouping against the real changed set (falling back to chore: update dotfiles for a missing subject), commits each group staging only its own files, and pushes. If the pre-commit hook rejects a group, its files are left unstaged, the remaining groups are still committed and pushed, and it exits 1 — so cmc && … does not mean the tree is clean. cmc --dry-run prints the plan and changes nothing; --no-push commits only. It never runs chezmoi re-add. Requires chezmoi, jq, gitleaks and claude or codex.

Editor intercept (~/.config/nvim/lua/chezmoi_workflow.lua, in the separate nvim repo). Opening a managed live path in neovim swaps the buffer to its chezmoi source and auto-applies on save, so hand edits land in the repo.

State and logs — all disposable, deleting any of it only forces a cold reload:

What Where
Drift state file + refresh scratch ~/.cache/chezmoi-drift/
Claude guard: managed-set cache, per-session state, log ~/.claude/.chezmoi-guard/{managed.json,sessions/,chezmoi-guard.log}
Codex guard: same layout ~/.codex/.tmp/chezmoi-guard/
opencode guard: log ~/.local/share/opencode/chezmoi-guard.log

Logs rotate once at 1 MB (.1); per-tool-call trace lines only appear with CHEZMOI_GUARD_DEBUG=1.


Maintainer ops

Day-to-day workflows for editing/adding/removing dotfiles, rotating secrets, multi-machine sync, etc. are documented in OPERATIONS.md.


Acknowledgments

Patterns and inspiration from:


License

MIT — see LICENSE.

About

Personal dotfiles managed with chezmoi (https://chezmoi.io)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages