DevOpsPaolo Castro

Managing Dotfiles with a Bare Git Repository

A clean approach to managing dotfiles across multiple machines using a bare Git repository. No symlinks, no special tools, just Git.

#git#dotfiles#productivity#workflow

Dotfiles are those configuration files that live in your home directory — .bashrc, .zshrc, .vimrc, and countless others that define your development environment. Over the years, I've tried various approaches to manage them: from manual copying between machines, to symlink-based tools, to dedicated dotfile managers. Each approach had its drawbacks.

A few years ago, I discovered a technique using a bare Git repository that solved all my pain points. Since then, I've used this setup across multiple machines, switching between macOS and Linux, and it has worked flawlessly.

The Problem with Common Approaches

Most dotfile management tools rely on storing your dotfiles in a dedicated directory (like ~/dotfiles) and then creating symlinks to their actual locations in your home directory. While this works, it introduces complexity:

  • You need to maintain symlink scripts
  • Tracking new files requires updating your setup
  • Some applications don't handle symlinks well
  • It's not immediately obvious which files are managed

I wanted something simpler: treat my home directory as a Git repository without the usual mess of having a .git folder cluttering my actual home directory.

The Solution: A Bare Repository

A bare Git repository stores all Git metadata but no working tree. By combining this with Git's --work-tree flag, we can track files in our home directory while keeping the Git metadata tucked away in a hidden folder.

Here's the approach:

  1. Create a bare repository in ~/.cfg
  2. Create a config alias that operates on this repository
  3. Use the alias just like you'd use git

Initial Setup

Starting fresh on a new machine is straightforward:

hljs bash
# Create the bare repository
git init --bare $HOME/.cfg

# Create an alias to interact with the repository
alias config='/usr/bin/git --git-dir=$HOME/.cfg --work-tree=$HOME'

# Hide untracked files (optional but recommended)
config config --local status.showUntrackedFiles no

# Add the alias to your shell config so it persists
echo "alias config='/usr/bin/git --git-dir=$HOME/.cfg --work-tree=$HOME'" >> $HOME/.bashrc

Now you can start tracking your dotfiles:

hljs bash
# Add files you want to track
config add .zshrc
config add .vimrc
config add .gitconfig

# Commit them
config commit -m "Initial dotfiles commit"

# Add your remote repository (create one on GitHub/GitLab first)
config remote add origin git@github.com:<username>/dotfiles.git

# Push your changes
config push -u origin main

Restoring on a New Machine

When setting up a new machine, you clone your dotfiles in one command:

hljs bash
# Clone the repository
git clone --bare git@github.com:<username>/dotfiles.git $HOME/.cfg

# Create the alias (before using it)
alias config='/usr/bin/git --git-dir=$HOME/.cfg --work-tree=$HOME'

# Checkout the actual content from the repository
config checkout

# Hide untracked files
config config --local status.showUntrackedFiles no

If checkout fails because some files already exist, you can back them up:

hljs bash
mkdir -p ~/.config-backup
config checkout 2>&1 | egrep "\s+\." | awk {'print $1'} | xargs -I{} mv {} .config-backup/{}
config checkout

Daily Usage

Once set up, you use config exactly like you'd use git:

hljs bash
# Check status
config status

# Add a new file
config add .bashrc

# Commit changes
config commit -m "Update bash configuration"

# Push to remote
config push

# Pull changes from another machine
config pull

# View history
config log

# See differences
config diff

The beauty of this approach is that it's just Git — no learning curve beyond the alias.

Real-World Experience

I've been using this setup for several years now, and it has survived:

  • Multiple machine switches: Moved between laptops at least four times
  • Parallel machines: Simultaneously used a MacBook for personal work and a Linux workstation
  • Cross-platform: Shared dotfiles between macOS and Linux with conditional logic where needed
  • Collaboration: Kept work and personal configs separate with different branches

The key advantages I've experienced:

  • No symlinks: Files live where they're expected
  • Selective tracking: Only track what you explicitly add
  • Standard Git workflow: Use branches, tags, and all Git features
  • Easy sharing: Push to a private repository, clone anywhere
  • Minimal overhead: Just an alias and a hidden directory

Considerations

A few things to keep in mind:

Security: Your dotfiles may contain sensitive information. Use a private repository and consider using tools like git-crypt for extra sensitive data, or simply don't commit files with secrets.

Platform differences: If you use both macOS and Linux, you might need conditional logic in your shell configs:

hljs bash
if [[ "$OSTYPE" == "darwin"* ]]; then
  # macOS-specific settings
else
  # Linux-specific settings
fi

Untracked files: Setting status.showUntrackedFiles no means config status won't show untracked files. This is intentional — otherwise, your entire home directory would appear as untracked. When you want to add new files, you'll need to specify them explicitly.

Conclusion

This bare repository approach has proven to be the most robust dotfile management solution I've used. It leverages Git's power without introducing additional complexity or dependencies. The setup takes five minutes, and then you can forget about it — it just works.

If you're tired of fighting with dotfile managers or manually syncing configurations between machines, give this approach a try. It might just be the last dotfile setup you'll ever need.

Want to read more?

View All Stories