For me, the turning point was spending a weekend setting up a new MacBook and realizing I couldn’t reproduce my environment reliably. That’s when I started managing my dotfiles properly.
After trying bare git repos, GNU Stow, and chezmoi, I settled on yadm — and it’s been my go-to for over a year.
Why Yadm? #
There are many dotfile managers. Here’s why yadm won:
| Tool | Approach | My Take |
|---|---|---|
| Bare git | Raw git with $HOME as work tree |
Works but fragile, no extras |
| GNU Stow | Symlink farm manager | Requires specific directory structure |
| chezmoi | Template-based with state management | Powerful but complex, uses its own DSL |
| yadm | Thin wrapper around git | Git-native, minimal learning curve, built-in extras |
yadm add, yadm commit, yadm push, yadm diff. If you know git, you know yadm.
What yadm adds on top:
- Alternate files: Different configs per machine using
##hostnameor##ossuffixes - Encryption: Encrypt sensitive files with GPG before pushing
- Bootstrap: Run a setup script on first clone
- Native
$HOMEtracking: No symlinks, files live where they belong
My Directory Structure #
Here’s what I track:
~
├── .zshrc # Shell config (Zsh + Zinit + Oh-My-Zsh)
├── .tmux.conf # tmux configuration
├── .config/
│ ├── ghostty/config # Ghostty terminal
│ ├── kitty/kitty.conf # Kitty terminal (backup)
│ ├── nvim/ # Neovim/LazyVim config
│ ├── starship.toml # Prompt
│ ├── atuin/config.toml # Shell history
│ ├── mise/config.toml # Version manager
│ └── ripgrep/config # Ripgrep defaults
├── .local/bin/ # Custom scripts
├── .Brewfile # Homebrew packages
└── .yadm/
└── hooks/pre-commit # Pre-commit validationThe .gitignore is crucial — you want to explicitly track only what you need:
# Ignore everything by default
*
# Then selectively un-ignore
!.zshrc
!.tmux.conf
!.config/ghostty/
!.config/nvim/
!.Brewfile
# ... etcAutomated Daily Maintenance #
-
Homebrew Update
Auto
brew update && brew upgrade && brew cleanup— keeps all packages fresh. -
Zinit Plugins
Auto
zsh -ic 'zinit update --all'— updates all Zsh plugins. -
Neovim via bob
Auto
bob update --all— updates Neovim version manager and builds. -
LazyVim Sync
Auto
nvim --headless "+Lazy! sync" +qa— syncs all LazyVim plugins. -
Cleanup
Auto
Removes broken symlinks in~/.local/bin. Tracks last run date to prevent duplicates.
The script:
- Tracks last run date to prevent duplicate runs
- Catches up if the laptop was off (runs on next login)
- Has quick aliases:
mr(run),ms(status),ml(logs)
I also have a control script for managing it:
daily-maintenance-control.sh start # Enable auto-run
daily-maintenance-control.sh stop # Disable
daily-maintenance-control.sh status # Check state
daily-maintenance-control.sh logs # View recent logsPre-Commit Testing #
Every yadm commit runs through a pre-commit hook:
#!/bin/bash
# .yadm/hooks/pre-commit
# Run the test suite
bash ~/test-dotfiles.sh
if [ $? -ne 0 ]; then
echo "Tests failed. Commit aborted."
exit 1
fiThe test suite (test-dotfiles.sh) validates:
This catches mistakes before they reach the repo. I never push broken configs.
Version Management with Mise #
Mise (formerly rtx) manages language runtimes across my machines:
# ~/.config/mise/config.toml
[tools]
node = "lts"
python = "latest"
go = "latest"
ruby = "latest"
[settings]
idiomatic_version_file_enable = true # Reads .nvmrc, .python-version, etc.
not_found_auto_install = true # Auto-install missing versions
jobs = 4 # Parallel installationsidiomatic_version_file_enable means mise respects .nvmrc, .python-version, and .tool-versions files in project directories. When I cd into a project that needs Node 18, mise automatically activates it.
Practical Tips #
1. Start Small #
Don’t try to track everything at once. Start with:
yadm add ~/.zshrc
yadm add ~/.config/ghostty/config
yadm commit -m "initial: shell and terminal config"Add more as you modify things.
2. Use Branches for Experiments #
yadm checkout -b experiment/new-shell-config
# Try things out...
yadm checkout main # Revert if it didn't work3. Bootstrap Script for New Machines #
Create a bootstrap that gets a fresh machine to your preferred state:
#!/bin/bash
# ~/.config/yadm/bootstrap
# Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install packages
brew bundle --file=~/.Brewfile
# Set default shell
chsh -s $(which zsh)
echo "Bootstrap complete. Restart your terminal."Then on a new machine:
yadm clone https://github.com/youruser/dotfiles.git
yadm bootstrap4. Keep Sensitive Data Out #
Use .gitignore aggressively and yadm’s encryption for anything sensitive:
# Encrypt SSH configs
yadm encryptGit credentials should go through Git Credential Manager, never in dotfiles.
5. Document Your Setup #
I keep a CLAUDE.md in my dotfiles repo — it documents the architecture, conventions, and mandatory rules. This serves as both documentation for myself and instructions for AI assistants helping me modify configs.
The Payoff #
With this setup:
- New machine setup — Clone + bootstrap, done in under an hour
- Daily updates — Automated, zero manual intervention
- Config changes — Tested before commit, never push broken configs
- Cross-machine sync —
yadm pullon any machine - Rollback — Full git history, revert any change
The initial investment is a few hours. The ongoing cost is near zero. And the peace of mind knowing your entire development environment is versioned, tested, and reproducible? Priceless.
Check out my full setup: