Dotfiles backup with a bare Git repo

· Tech

I have configuration files scattered across my home directory: Hammerspoon, Helix, tmux, lazygit, Ghostty, zsh, and more. I wanted a simple way to back them up to a private GitHub repo without symlinks, without extra tools, and without accidentally tracking my entire home directory.

A bare Git repo does exactly this. No dependencies, no wrapper tools, just Git.

This post is part of my terminal workflow series.

How it works

A normal git init creates a .git folder in the current directory and tracks everything under it. A bare Git repo separates the Git database from the working tree. The database lives in ~/projects/.dotfiles, the working tree is your entire $HOME, but it only tracks files you explicitly add. Everything else is invisible to it.

Setup

Step 1: Create the bare repo

Terminal window
git init --bare ~/projects/.dotfiles

Step 2: Add the alias

Add this to your ~/.zshrc:

Terminal window
alias dot='git --git-dir=$HOME/projects/.dotfiles --work-tree=$HOME'

Reload your shell:

Terminal window
source ~/.zshrc

Step 3: Hide untracked files

This is the key step. Without it, dot status would list every single file in your home directory.

Terminal window
dot config status.showUntrackedFiles no

Step 4: Create the remote repo

Create a private repo on GitHub (e.g. dotfiles), then:

Terminal window
dot remote add origin [email protected]:YOUR_USERNAME/dotfiles.git
dot branch -M main

Step 5: Add your config files

Only the files you explicitly add get tracked. Nothing else.

Terminal window
dot add ~/.zshrc
dot add ~/.config/tmux/tmux.conf
dot add ~/.config/helix/config.toml
dot add ~/.config/helix/languages.toml
dot add ~/.config/lazygit/config.yml
dot add ~/.config/ghostty/config
dot add ~/.hammerspoon/init.lua
dot add ~/Library/LaunchAgents/com.local.capslock-remap.plist

Commit and push:

Terminal window
dot commit -m "initial dotfiles"
dot push -u origin main

Step 6: One command to back up everything

Add this function to your ~/.zshrc:

Terminal window
dotpush() {
dot add -u
dot commit -m "update $(date +%Y-%m-%d)"
dot push
}

dot add -u stages changes only to files that are already tracked. It will never pick up new files, only updates and deletions to files you previously added. So if you create a new config file, you still need to dot add it once manually. After that, dotpush handles it.

Reload your shell:

Terminal window
source ~/.zshrc

From now on, run dotpush and all your tracked configs get committed and pushed with today’s date.

Adding new files later

Whenever you set up a new tool or change where a config lives:

Terminal window
dot add ~/.config/new-tool/config.toml
dot commit -m "add new-tool config"
dot push

After that first dot add, the file is tracked and dotpush will pick up future changes automatically.

Checking what is tracked

Terminal window
dot ls-files

This lists every file the repo knows about. Useful to verify you have not missed anything, or accidentally added something you should not.

To see what changed since the last commit:

Terminal window
dot status
dot diff

Restoring on a new Mac

Step 1: Clone the bare repo

Terminal window
git clone --bare [email protected]:YOUR_USERNAME/dotfiles.git ~/projects/.dotfiles

Step 2: Add the alias

Terminal window
alias dot='git --git-dir=$HOME/projects/.dotfiles --work-tree=$HOME'

Step 3: Checkout

Terminal window
dot checkout

If this fails with an error about existing files (e.g. macOS creates a default .zshrc), back them up first:

Terminal window
mkdir -p ~/projects/.dotfiles-backup
dot checkout 2>&1 | grep "^\t" | sed 's/^\t//' | while read -r file; do
mkdir -p ~/projects/.dotfiles-backup/$(dirname "$file")
mv "$file" ~/projects/.dotfiles-backup/"$file"
done
dot checkout

This moves conflicting files to ~/projects/.dotfiles-backup so you can compare them later.

Step 4: Hide untracked files

Terminal window
dot config status.showUntrackedFiles no

Step 5: Verify

Terminal window
dot ls-files

All your configs are in place. Install your tools (brew install --cask hammerspoon, etc.) and everything picks up its config automatically.

Full restore script

For convenience, save this as a gist or keep it in your repo README. Run it on a fresh Mac after installing Git and setting up SSH keys:

Terminal window
git clone --bare [email protected]:YOUR_USERNAME/dotfiles.git ~/projects/.dotfiles
alias dot='git --git-dir=$HOME/projects/.dotfiles --work-tree=$HOME'
# Back up any conflicting files
mkdir -p ~/projects/.dotfiles-backup
dot checkout 2>&1 | grep "^\t" | sed 's/^\t//' | while read -r file; do
mkdir -p ~/projects/.dotfiles-backup/$(dirname "$file")
mv "$file" ~/projects/.dotfiles-backup/"$file"
done
dot checkout
dot config status.showUntrackedFiles no
echo "Dotfiles restored. Conflicts (if any) moved to ~/projects/.dotfiles-backup"

What not to track

Do not add files that contain secrets: SSH keys, API tokens, .env files. The repo is private, but secrets do not belong in Git. If you need to back up secrets, use a password manager or encrypted storage.

Also avoid large or machine-specific files like caches, logs, or anything under ~/Library that is not a LaunchAgent or a config plist you explicitly manage.

File locations

~/projects/.dotfiles/ # bare Git database (hidden, do not touch)
~/.zshrc # contains the dot alias and dotpush function

The whole point is that your config files stay exactly where they are. No symlinks, no special directories, no wrapper tools. Just Git.