| Tool | Stars | Symlinks | Copies | Templates | Multi-machine | Dependencies | Complexity |
|---|---|---|---|---|---|---|---|
| chezmoi | 18.5k | No | Yes | Yes (Go) | Yes | Go binary | Medium |
| dotbot | 7.8k | Yes | No | No | Via profiles | Python | Low |
| yadm | 6.2k | No | In-place | Yes | Yes (alts) | Git + Bash | Low |
| rcm | 3.2k | Yes | No | No | Yes (tags) | Shell | Low |
| homesick | 2.5k | Yes | No | No | No | Ruby | Low |
| GNU Stow | ~800 | Yes | No | No | Manual | Perl | Very Low |
| Bare git repo | N/A | No | In-place | No | No | Git only | Very Low |
| Approach | Examples | Pros | Cons |
|---|---|---|---|
| Bare git repo | Manual alias | Zero deps, files stay in place | No templating/encryption |
| Symlink managers | dotbot, rcm, homesick, GNU Stow | Simple mental model | Some apps overwrite symlinks with regular files |
| Git wrappers | yadm, chezmoi | Full git + extras | Learning curve for extras |
| Flat repo (Perham) | Clone + manual | Dead simple, zero abstraction | Manual symlink management |
| Config management | Ansible, Nix | Full system provisioning | Overkill for just dotfiles |
Symlinks: Single source of truth, instant sync, no drift. But: some apps don't detect changes via symlinks; some overwrite with regular files; backup tools may not follow.
Copies: Full FS notification support; apps can't break the link; works everywhere. But: two copies to keep in sync; conflict detection needed; possible drift.
Hybrid: Symlinks by default, copies only for apps that need them (per-entry flag).
git init --bare ~/.dotfiles.git
alias dot='git --git-dir=$HOME/.dotfiles.git --work-tree=$HOME'
dot config status.showUntrackedFiles noFiles stay in native locations. dot add ~/.zshrc && dot commit. That's it.
No manifest, no mapping, no sync script.
But: no copies support, no templating, no host-specific variants without branches.
- 270+ path mappings in
dotfiles.json, 324-line Ruby sync engine (bin/dotfiles) - Bidirectional copy with mtime-based conflict detection
- Commands:
load,save,status,config; options:--dry-run,--verbose,--prune - Strengths: explicit JSON config, bidirectional sync, dry-run safety, directory support
- Pain points: manual manifest maintenance, mtime-only conflicts, no rollback, Sublime Text bloat (54 entries)
Zero deps, files live in place. But no copies support (files ARE originals), no templating.
Uses copies (like current). Templates, encryption, 1Password. But Go dep, learning curve, own conventions.
Replace 270-entry JSON with directory-based discovery. Repo mirrors $HOME structure. Add a file = put it in the right place in repo. Script walks tree, copies to/from $HOME.
dotfiles status # show drift
dotfiles save # $HOME → repo
dotfiles load # repo → $HOME
dotfiles add FILE # start tracking a file
- Track a new config file
- Untrack a file
- See what's changed (diff)
- Sync changes to/from the repo
- Bootstrap a new machine from scratch
- Propagate changes across machines
- Handle machine-specific overrides
- Resolve conflicts
- Safety: preview changes before applying
- Safety: roll back a bad change
- Safety: avoid accidentally losing local edits
- Maintenance: add/remove groups of related configs
- Maintenance: keep package/tool dependencies in sync
- Maintenance: clean up stale files
- Rare: migrate configs when an app renames its config path
- Rare: share a subset of configs with someone
- Rare: handle secrets/credentials separately from public configs
chezmoi
| # | Use Case | Command |
|---|---|---|
| 1 | Track a new file | chezmoi add ~/.config/foo |
| 2 | Untrack a file | chezmoi forget ~/.config/foo |
| 3 | See what's changed | chezmoi diff |
| 4 | Sync changes | chezmoi apply (source→target)chezmoi re-add (target→source)chezmoi git -- commit -am "msg"chezmoi git -- push |
| 5 | Bootstrap new machine | chezmoi init --apply <github-user> |
| 6 | Propagate across machines | Source: chezmoi git -- commit -am "msg" && chezmoi git -- pushTarget: chezmoi update |
| 7 | Machine-specific overrides | chezmoi add --template ~/.config/fooUse .chezmoi.hostname, .chezmoi.os in .tmpl files |
| 8 | Resolve conflicts | chezmoi merge <file>chezmoi merge-all |
| 9 | Preview changes | chezmoi diffchezmoi apply --dry-run --verbose |
| 10 | Roll back | chezmoi git -- revert HEADchezmoi apply |
| 11 | Avoid losing edits | chezmoi diff before applychezmoi re-add to pull target changes backchezmoi merge for conflicts |
| 12 | Groups of configs | chezmoi add ~/.config/nvim (whole dir)chezmoi forget ~/.config/nvim (remove).chezmoiignore for exclusions |
| 13 | Package dependencies | run_onchange_ scripts: re-run when content or watched file hash changes |
| 14 | Clean up stale files | .chezmoiremove to declare removable fileschezmoi unmanaged to find orphans |
| 15 | Migrate config path | chezmoi forget <old-path>chezmoi add <new-path>Or use a run_once_ migration script |
| 16 | Share subset | .chezmoiignore with templates to conditionally ignore per machine.chezmoiexternal.toml to pull from separate repos |
| 17 | Secrets | chezmoi add --encrypt ~/.ssh/id_rsa (age/gpg)Template functions for 1Password, Bitwarden, gopass, Vault, etc. |
yadm
| # | Use Case | Command |
|---|---|---|
| 1 | Track a new file | yadm add ~/.config/foo |
| 2 | Untrack a file | yadm rm --cached ~/.config/foo |
| 3 | See what's changed | yadm diffyadm status |
| 4 | Sync changes | yadm add -u && yadm commit -m "msg" && yadm pushyadm pull |
| 5 | Bootstrap new machine | yadm clone <repo-url> (auto-runs bootstrap if present) |
| 6 | Propagate across machines | Source: yadm push. Target: yadm pull |
| 7 | Machine-specific overrides | Alternate files: file##os.Linux, file##hostname.myhost, file##class.Workyadm config local.class WorkJinja-like templates: file##template |
| 8 | Resolve conflicts | yadm mergetoolor manual: yadm add <file> && yadm commit |
| 9 | Preview changes | yadm diffIncoming: yadm fetch && yadm diff origin/main |
| 10 | Roll back | yadm revert HEADyadm checkout -- <file> |
| 11 | Avoid losing edits | yadm stash before pullyadm diff to check |
| 12 | Groups of configs | yadm add ~/.config/nvim -Ayadm rm -r --cached ~/.config/nvim (remove)Use .gitignore |
| 13 | Package dependencies | ~/.config/yadm/bootstrap script. Runs on yadm clone, manually via yadm bootstrap |
| 14 | Clean up stale files | N/A natively Manual: yadm rm <file> && rm <file> |
| 15 | Migrate config path | yadm rm --cached <old> && yadm add <new> |
| 16 | Share subset | Alternate files (##class.Personal / ##class.Work). Or separate branches |
| 17 | Secrets | yadm encrypt (GPG, files listed in ~/.config/yadm/encrypt)yadm decrypt to restoreAlso supports git-crypt/transcrypt |
dotbot
| # | Use Case | Command |
|---|---|---|
| 1 | Track a new file | Add to install.conf.yaml: - link: { ~/.vimrc: vimrc }Copy file into dotfiles dir |
| 2 | Untrack a file | Remove entry from install.conf.yamlManually delete the symlink |
| 3 | See what's changed | N/A built-in. Use git diff in dotfiles repo |
| 4 | Sync changes | ./install or dotbot -c install.conf.yaml |
| 5 | Bootstrap new machine | git clone --recursive <repo> ~/.dotfiles && cd ~/.dotfiles && ./install |
| 6 | Propagate across machines | cd ~/.dotfiles && git pull && ./install |
| 7 | Machine-specific overrides | dotbot -c install.conf.yaml -c host-specific.yamlConditionals: if: "[ $(hostname) = myhost ]" |
| 8 | Resolve conflicts | force: true in link directiverelink: true to replace old symlinkbackup: true to back up existing file |
| 9 | Preview changes | dotbot -c install.conf.yaml --dry-run |
| 10 | Roll back | N/A nativelygit revert then re-run ./install- clean: ["~"] removes dead symlinks |
| 11 | Avoid losing edits | backup: true preserves existing files as .dotbot-backup.<timestamp>Without force, refuses to overwrite non-symlink files |
| 12 | Groups of configs | Multiple config files: dotbot -c base.yaml -c vim.yaml -c zsh.yaml |
| 13 | Package dependencies | - shell: ["brew bundle --file=Brewfile", "Install packages"]Plugins: dotbot-brew, dotbot-apt |
| 14 | Clean up stale files | - clean: ["~"] removes dead symlinks- clean: { ~/.config: { recursive: true } } for recursive |
| 15 | Migrate config path | Update path in install.conf.yaml. Old dead symlink cleaned by - clean: ["~"] |
| 16 | Share subset | Separate config files: dotbot -c shared.yaml |
| 17 | Secrets | N/A natively. Use git-crypt/transcrypt on specific files |
rcm
| # | Use Case | Command |
|---|---|---|
| 1 | Track a new file | mkrc ~/.vimrc (moves to ~/.dotfiles/vimrc, symlinks back) |
| 2 | Untrack a file | rcdn ~/.vimrc (removes symlink)Manually remove from dotfiles dir |
| 3 | See what's changed | lsrc (lists managed files)lsrc -F shows status symbolsNo built-in diff |
| 4 | Sync changes | rcup (re-syncs all)rcup -v for verbose |
| 5 | Bootstrap new machine | git clone <repo> ~/.dotfiles && RCRC=~/.dotfiles/rcrc rcup |
| 6 | Propagate across machines | cd ~/.dotfiles && git pull && rcup |
| 7 | Machine-specific overrides | mkrc -o ~/.rcrc (stores in host-$HOSTNAME/)mkrc -t work ~/.gitconfig (tag-based)Set in ~/.rcrc: TAGS="work laptop" |
| 8 | Resolve conflicts | rcup -f (force)rcup -i (interactive, default) |
| 9 | Preview changes | lsrc to see what would be linkedrcup -g prints shell script without executing |
| 10 | Roll back | rcdn (removes all symlinks)git revert in repo, then rcup |
| 11 | Avoid losing edits | rcup -i prompts on conflicts (default)COPY_ALWAYS="ssh/id_*" copies instead of symlinking |
| 12 | Groups of configs | mkrc -t python ~/.pylintrcrcup -t python (install)rcdn -t python (remove) |
| 13 | Package dependencies | Hooks: ~/.dotfiles/hooks/post-up script runs on every rcup |
| 14 | Clean up stale files | N/A natively. Compare lsrc output against actual symlinks |
| 15 | Migrate config path | rcdn, rename file in dotfiles dir, rcup |
| 16 | Share subset | Multiple dirs: DOTFILES_DIRS="~/.dotfiles ~/shared-dotfiles" in ~/.rcrc |
| 17 | Secrets | DOTFILES_DIRS="~/.dotfiles ~/.dotfiles-private"COPY_ALWAYS="ssh/id_*" for sensitive files |
homesick
| # | Use Case | Command |
|---|---|---|
| 1 | Track a new file | homesick track ~/.config/foo castle-name |
| 2 | Untrack a file | N/A (manually remove from castle's home/ dir and delete symlink) |
| 3 | See what's changed | homesick diff castle-namehomesick status castle-name |
| 4 | Sync changes | homesick pull castle-namehomesick commit castle-name "msg"homesick push castle-name |
| 5 | Bootstrap new machine | gem install homesickhomesick clone user/castlehomesick link castle-name |
| 6 | Propagate across machines | Source: homesick commit castle-name "msg" then homesick push castle-nameTarget: homesick pull castle-name then homesick link castle-name |
| 7 | Machine-specific overrides | Separate castles per machine Clone multiple, link in order (last wins) No native conditionals |
| 8 | Resolve conflicts | homesick link --force castle-name (overwrites without prompting) |
| 9 | Preview changes | N/A nativelyhomesick diff castle-name for uncommitted changes, but no dry-run for link |
| 10 | Roll back | N/A nativelyhomesick cd castle-name then git revert/git checkout |
| 11 | Avoid losing edits | homesick link prompts on conflicts by default (unless --force) |
| 12 | Groups of configs | Separate castles per grouphomesick clone user/vim-castlehomesick link vim-castlehomesick destroy castle-name (remove) |
| 13 | Package dependencies | .homesickrc at castle root. Run with homesick rc castle-name |
| 14 | Clean up stale files | homesick unlink castle-name removes all symlinks for that castle. No stale-file detection |
| 15 | Migrate config path | N/A. Manually move file inside castle's home/ dir, re-link |
| 16 | Share subset | Dedicated castle for the subset, push to separate repo |
| 17 | Secrets | Use a private castle. No native encryption |
GNU Stow
| # | Use Case | Command |
|---|---|---|
| 1 | Track a new file | mv ~/.vimrc ~/dotfiles/vim/.vimrcstow -d ~/dotfiles -t ~ vim |
| 2 | Untrack a file | stow -D -t ~ vim (unstow package)Restructure package to exclude the file, re-stow |
| 3 | See what's changed | N/A built-in. Use git diff in dotfiles repo |
| 4 | Sync changes | stow -R -t ~ vim (restow: unstow then stow, picks up new files) |
| 5 | Bootstrap new machine | git clone <repo> ~/dotfiles && cd ~/dotfiles && stow -t ~ vim zsh git ... |
| 6 | Propagate across machines | cd ~/dotfiles && git pull && stow -R -t ~ vim zsh git ... |
| 7 | Machine-specific overrides | Separate packages per host: ~/dotfiles/vim-work/, ~/dotfiles/vim-personal/. Stow selectively |
| 8 | Resolve conflicts | stow --adopt -t ~ vim (moves conflicting files INTO package dir, then symlinks). Warning: overwrites repo versions |
| 9 | Preview changes | stow -n -v -t ~ vim (-n = simulate, -v = verbose) |
| 10 | Roll back | stow -D -t ~ vim (unstow). Then git revert in repo, re-stow |
| 11 | Avoid losing edits | Default: refuses to overwrite (reports conflict)--adopt pulls existing files into packageAlways preview with -n -v first |
| 12 | Groups of configs | Each package is a group: stow -t ~ vim zsh git / stow -D -t ~ vim zsh |
| 13 | Package dependencies | N/A natively. Use a Makefile or bootstrap script alongside stow |
| 14 | Clean up stale files | stow -R -t ~ vim (restow clears stale links). Stow only manages its own symlinks |
| 15 | Migrate config path | stow -D -t ~ oldpkg, restructure package dir, stow -t ~ newpkg |
| 16 | Share subset | Separate stow dir: stow -d ~/shared-dotfiles -t ~ git vim |
| 17 | Secrets | Separate stow dir for secrets in an encrypted repo. No native encryption |
Bare git repo
Setup: git init --bare $HOME/.dotfiles.git && alias dot='git --git-dir=$HOME/.dotfiles.git --work-tree=$HOME' && dot config status.showUntrackedFiles no
| # | Use Case | Command |
|---|---|---|
| 1 | Track a new file | dot add ~/.config/foo && dot commit -m "track foo" |
| 2 | Untrack a file | dot rm --cached ~/.config/foo (keeps local, removes from repo) |
| 3 | See what's changed | dot diffdot status |
| 4 | Sync changes | dot add -u && dot commit -m "msg" && dot pushdot pull |
| 5 | Bootstrap new machine | git clone --bare <url> $HOME/.dotfiles.git && dot checkout (back up conflicts first if needed) |
| 6 | Propagate across machines | Source: dot add -u && dot commit -m "msg" && dot pushTarget: dot pull |
| 7 | Machine-specific overrides | Branches per machine: dot checkout -b machine-name. No native mechanism |
| 8 | Resolve conflicts | dot mergedot mergetooldot checkout --theirs/--ours <file> |
| 9 | Preview changes | dot fetch && dot diff HEAD..origin/main |
| 10 | Roll back | dot revert <commit>dot checkout <commit> -- <file> |
| 11 | Avoid losing edits | dot stash before pulldot status to check |
| 12 | Groups of configs | dot add ~/.config/nvim/ ~/.config/alacritty/dot rm --cached -r ~/.config/nvim/ (remove) |
| 13 | Package dependencies | N/A natively. Track a Brewfile and run manually |
| 14 | Clean up stale files | dot ls-files to see tracked files; manually compare. Limited since untracked files are hidden |
| 15 | Migrate config path | dot mv ~/.old/path ~/.new/path && dot commit -m "migrate" |
| 16 | Share subset | Separate bare repodot archive HEAD -- .config/nvim |
| 17 | Secrets | .gitignore for exclusiondot crypt init && dot crypt add-gpg-user <key-id> (git-crypt) |
- Convention over configuration — directory structure IS the manifest
- Additive safety — never delete without explicit --prune
- Diff before apply — always show what will change
- Atomic operations — backup before overwrite
- Host-specific overrides — suffix convention or overlay dirs