TL;DR:

  • A bare Git repo is the zero-dependency option — one alias, your existing Git knowledge, done; best for a single machine or when you want no extra tooling
  • GNU Stow gives you logical package grouping via symlinks with minimal overhead — good for people who want modular configs without learning a new tool
  • Chezmoi is the right choice for multiple machines with different configurations — Go templating, encrypted secrets, and first-class multi-machine support justify the learning curve

Every developer eventually hits the same friction: a new machine, a reinstall, or a colleague whose workflow you want to replicate, and suddenly your carefully tuned shell prompt and Neovim config are nowhere. Dotfile management is the solution, and the three approaches below cover the full spectrum from “I want this done in five minutes” to “I maintain configs across six machines with different requirements.”

Approach 1: Bare Git Repo

The bare Git repo approach requires no tools beyond Git itself. It works by initialising a bare repository — one without a working tree — and then aliasing a Git command to use your $HOME directory as the working tree against that repository.

Setup

git init --bare $HOME/.dotfiles
alias dotfiles='/usr/bin/git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'
dotfiles config --local status.showUntrackedFiles no

That last line is important: without it, dotfiles status shows every untracked file in your home directory, which is noise you do not want.

Add the alias to your shell rc so it persists:

echo "alias dotfiles='/usr/bin/git --git-dir=\$HOME/.dotfiles/ --work-tree=\$HOME'" >> ~/.zshrc

Then track files by adding them explicitly:

dotfiles add ~/.zshrc
dotfiles add ~/.config/nvim/init.lua
dotfiles add ~/.gitconfig
dotfiles commit -m "Initial dotfiles"
dotfiles remote add origin git@github.com:you/dotfiles.git
dotfiles push -u origin main

Restoring on a new machine

git clone --bare git@github.com:you/dotfiles.git $HOME/.dotfiles
alias dotfiles='/usr/bin/git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'
dotfiles checkout
dotfiles config --local status.showUntrackedFiles no

If checkout fails because existing files conflict (a fresh install often ships a default .bashrc), back them up and retry:

dotfiles checkout 2>&1 | grep "^\s" | awk '{print $1}' | xargs -I{} mv {} {}.bak
dotfiles checkout

When it works well

Single machine or identical machines. You want the absolute minimum overhead. You already know Git and do not want to learn anything new. Everything is just Git — dotfiles log, dotfiles diff, dotfiles stash all work exactly as you expect.

Trade-offs

No templating whatsoever. If your .gitconfig email needs to be different at work versus at home, you are choosing one or the other, or maintaining two branches. Conflicts with existing files on fresh installs need manual resolution. The showUntrackedFiles no setting helps, but dotfiles status output can still be confusing until you are used to the workflow.


Approach 2: GNU Stow

GNU Stow is a symlink farm manager — it was built for managing compiled software packages before package managers existed, but it maps perfectly onto dotfile management. The idea is to organise your dotfiles into named “package” directories, then use Stow to create symlinks from those directories into a target location (by default, the parent of where Stow is run from).

Setup

Create a ~/dotfiles directory and organise your configs as packages:

~/dotfiles/
  zsh/
    .zshrc
    .zsh_aliases
  git/
    .gitconfig
  nvim/
    .config/
      nvim/
        init.lua
        lua/
          plugins.lua

Each top-level directory under ~/dotfiles is a package. The directory structure inside each package mirrors where the files should land in $HOME.

Install GNU Stow:

# macOS
brew install stow

# Debian/Ubuntu
sudo apt install stow

# Arch
sudo pacman -S stow

Then apply packages:

cd ~/dotfiles
stow zsh       # creates ~/.zshrc → ~/dotfiles/zsh/.zshrc
stow git       # creates ~/.gitconfig → ~/dotfiles/git/.gitconfig
stow nvim      # creates ~/.config/nvim → ~/dotfiles/nvim/.config/nvim

Or apply everything at once:

stow */

The underlying directory is a regular Git repo:

cd ~/dotfiles
git init
git add .
git commit -m "Initial dotfiles"
git remote add origin git@github.com:you/dotfiles.git
git push -u origin main

Restoring on a new machine

git clone git@github.com:you/dotfiles.git ~/dotfiles
cd ~/dotfiles
stow zsh git nvim   # or stow */

When it works well

You think in terms of modular configurations — “apply my zsh setup but not my work Slack config” is a natural operation. You want the simplicity of symlinks without thinking about them manually. The package model also makes it easy to share specific configs without sharing everything.

Trade-offs

No templating. Machine-specific config means either separate package directories (a git-work/ and a git-personal/ package that you selectively stow) or maintaining branches. Symlink conflicts — when a file already exists at the target location — require manual resolution before Stow can proceed. The --adopt flag can help but is destructive if used carelessly (it moves the existing file into your Stow directory and replaces it with a symlink, which can overwrite your managed version). Test with stow --simulate (or -n) before running for real.


Approach 3: Chezmoi

Chezmoi is a dedicated dotfile manager built around the problems that bare repos and Stow deliberately do not solve: machine-specific configuration, encrypted secrets, and bootstrapping a new machine from scratch. It is more complex than the other two approaches, but the complexity pays for itself once you manage configs across more than one machine.

Setup

# Install
sh -c "$(curl -fsLS get.chezmoi.io)"

# Initialise (creates ~/.local/share/chezmoi)
chezmoi init

# Add files
chezmoi add ~/.zshrc
chezmoi add ~/.gitconfig
chezmoi add ~/.config/nvim/init.lua

Chezmoi copies files into its source directory (~/.local/share/chezmoi) with a naming convention that encodes metadata: dot_zshrc for .zshrc, dot_config/nvim/init.lua for ~/.config/nvim/init.lua. The naming feels unusual at first but becomes second nature.

Apply changes from the source state to your home directory:

chezmoi apply

Preview what would change without applying:

chezmoi diff

Edit a managed file through chezmoi (edits the source, not the symlink):

chezmoi edit ~/.zshrc

Templating for machine-specific config

This is where chezmoi earns its complexity. Files with a .tmpl extension are processed through Go’s text/template engine before being written. Chezmoi populates template variables from a ~/.config/chezmoi/chezmoi.toml config and from chezmoi data (queried from the system).

A common example is a .gitconfig that uses a different email depending on the machine:

# ~/.local/share/chezmoi/dot_gitconfig.tmpl
[user]
    name = Your Name
    email = {{ if eq .chezmoi.hostname "work-laptop" }}you@company.com{{ else }}you@personal.com{{ end }}

[core]
    editor = nvim

You can also use if blocks for entire sections:

{{ if eq .chezmoi.os "darwin" }}
[credential]
    helper = osxkeychain
{{ end }}

Run chezmoi data to see all available template variables, including hostname, OS, username, and any custom variables you define in chezmoi.toml.

Secrets management

Chezmoi integrates natively with 1Password, Bitwarden, Vault, and others. For 1Password:

# In a template file
[github]
    token = {{ onepasswordRead "op://Personal/GitHub Token/password" }}

For files that should be encrypted at rest in the repo, chezmoi supports age encryption:

chezmoi add --encrypt ~/.ssh/id_ed25519.pub

The encrypted file is committed to the repo; chezmoi decrypts it on apply using your age key.

Restoring on a new machine

sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply git@github.com:you/dotfiles.git

One command clones your repo, applies your configs, and (with a run_once_ bootstrap script) can install packages, set up your shell, and configure the machine from scratch.

When it works well

Multiple machines with meaningfully different configurations — different OSes, work versus personal email, machine-specific secrets. You want encrypted secrets in your repo without manual gpg key management. You want a single command to bootstrap a new machine.

Trade-offs

Chezmoi has its own directory structure and naming conventions that take time to internalise. chezmoi edit instead of editing files directly is a workflow change. The Go templating syntax is not universally loved. If you only have one machine and your configs do not vary, chezmoi’s complexity buys you nothing.


Choosing Between Them

Bare Git RepoGNU StowChezmoi
DependenciesGit onlyGit + StowGit + Chezmoi
TemplatingNoNoYes (Go templates)
Encrypted secretsNoNoYes (age, 1Password, etc.)
Machine-specific configBranchesMultiple packagesFirst-class
New machine bootstrapManualSemi-manualOne command
Learning curveLowLowMedium

Use a bare repo if you are on one machine, want zero new tools, and your configs do not vary. This is the right starting point for most people.

Use GNU Stow if you think in modular packages, want to selectively apply subsets of your configs, and do not need templating. Good middle ground for developers who prefer simple, composable tools.

Use Chezmoi if you manage configs across multiple machines with different requirements, want secrets in the repo encrypted at rest, or want a one-command bootstrap for new machines.

Community Resources

  • dotfiles.github.io — curated list of dotfile repos from across the community, searchable by tool
  • r/unixporn — despite the name, the primary community for terminal aesthetics and sharing configs
  • chezmoi.io — chezmoi’s documentation is well-written and covers the full feature set including the bootstrapping run_once_ scripts
  • github.com/webpro/awesome-dotfiles — aggregated tooling and resource list