Skip to content

Instantly share code, notes, and snippets.

@dreikanter
Last active March 14, 2026 11:41
Show Gist options
  • Select an option

  • Save dreikanter/1d7e634906100d347c06861dc3731f0d to your computer and use it in GitHub Desktop.

Select an option

Save dreikanter/1d7e634906100d347c06861dc3731f0d to your computer and use it in GitHub Desktop.
Dotfiles Management Research

Dotfiles Management Research

Tools Landscape

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

Approaches Taxonomy

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 vs Copies

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).

Bare Git Repo Technique

git init --bare ~/.dotfiles.git
alias dot='git --git-dir=$HOME/.dotfiles.git --work-tree=$HOME'
dot config status.showUntrackedFiles no

Files 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.

Current Setup Summary

  • 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)

Three Serious Contenders

1. Bare Git Repo — zero abstraction

Zero deps, files live in place. But no copies support (files ARE originals), no templating.

2. chezmoi — full-featured

Uses copies (like current). Templates, encryption, 1Password. But Go dep, learning curve, own conventions.

3. Streamlined Custom Script — simplified current approach

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

Use Cases by Tool

Use cases

  1. Track a new config file
  2. Untrack a file
  3. See what's changed (diff)
  4. Sync changes to/from the repo
  5. Bootstrap a new machine from scratch
  6. Propagate changes across machines
  7. Handle machine-specific overrides
  8. Resolve conflicts
  9. Safety: preview changes before applying
  10. Safety: roll back a bad change
  11. Safety: avoid accidentally losing local edits
  12. Maintenance: add/remove groups of related configs
  13. Maintenance: keep package/tool dependencies in sync
  14. Maintenance: clean up stale files
  15. Rare: migrate configs when an app renames its config path
  16. Rare: share a subset of configs with someone
  17. 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 -- push
Target: chezmoi update
7 Machine-specific overrides chezmoi add --template ~/.config/foo
Use .chezmoi.hostname, .chezmoi.os in .tmpl files
8 Resolve conflicts chezmoi merge <file>
chezmoi merge-all
9 Preview changes chezmoi diff
chezmoi apply --dry-run --verbose
10 Roll back chezmoi git -- revert HEAD
chezmoi apply
11 Avoid losing edits chezmoi diff before apply
chezmoi re-add to pull target changes back
chezmoi 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 files
chezmoi 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 diff
yadm status
4 Sync changes yadm add -u && yadm commit -m "msg" && yadm push
yadm 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.Work
yadm config local.class Work
Jinja-like templates: file##template
8 Resolve conflicts yadm mergetool
or manual: yadm add <file> && yadm commit
9 Preview changes yadm diff
Incoming: yadm fetch && yadm diff origin/main
10 Roll back yadm revert HEAD
yadm checkout -- <file>
11 Avoid losing edits yadm stash before pull
yadm diff to check
12 Groups of configs yadm add ~/.config/nvim -A
yadm 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 restore
Also 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.yaml
Manually 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.yaml
Conditionals: if: "[ $(hostname) = myhost ]"
8 Resolve conflicts force: true in link directive
relink: true to replace old symlink
backup: true to back up existing file
9 Preview changes dotbot -c install.conf.yaml --dry-run
10 Roll back N/A natively
git 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 symbols
No 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 linked
rcup -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 ~/.pylintrc
rcup -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-name
homesick status castle-name
4 Sync changes homesick pull castle-name
homesick commit castle-name "msg"
homesick push castle-name
5 Bootstrap new machine gem install homesick
homesick clone user/castle
homesick link castle-name
6 Propagate across machines Source: homesick commit castle-name "msg" then homesick push castle-name
Target: 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 natively
homesick diff castle-name for uncommitted changes, but no dry-run for link
10 Roll back N/A natively
homesick 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 group
homesick clone user/vim-castle
homesick link vim-castle
homesick 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/.vimrc
stow -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 package
Always 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 diff
dot status
4 Sync changes dot add -u && dot commit -m "msg" && dot push
dot 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 push
Target: dot pull
7 Machine-specific overrides Branches per machine: dot checkout -b machine-name. No native mechanism
8 Resolve conflicts dot merge
dot mergetool
dot 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 pull
dot 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 repo
dot archive HEAD -- .config/nvim
17 Secrets .gitignore for exclusion
dot crypt init && dot crypt add-gpg-user <key-id> (git-crypt)

Design Principles (from research)

  1. Convention over configuration — directory structure IS the manifest
  2. Additive safety — never delete without explicit --prune
  3. Diff before apply — always show what will change
  4. Atomic operations — backup before overwrite
  5. Host-specific overrides — suffix convention or overlay dirs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment