---
title: "Dotfiles: A Git-Based Strategy for Configuration Management"
description: "Discover the ultimate strategy for managing your dotfiles using a bare Git repository, simplifying the process of keeping your configuration files synchronized and secure across multiple machines."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/blog/post/dotfiles
---

# Dotfiles: A Git-Based Strategy for Configuration Management

## Introduction

Your dotfiles those hidden `.`-prefixed configuration files scattered across your home directory are the muscle memory of your environment. They hold your shell aliases, your editor settings, your Git identity, your terminal theme. Lose them and a fresh machine feels like someone else's computer. Version them, and any machine becomes _yours_ in a single clone.

This guide covers two battle-tested, Git-based strategies for managing them:

  Both approaches use plain Git no extra runtime, no proprietary format. They
  differ only in **how the files reach your home directory**: the bare
  repository checks them out in place, while the modular repository symlinks
  them in from a clone you control.

| Strategy                      | How files land in `$HOME`      | Best when                                          |
| ----------------------------- | ------------------------------ | -------------------------------------------------- |
| **Bare Git repository**       | Checked out directly into home | You want zero tooling and zero symlinks            |
| **Modular repo + bootstrap**  | Symlinked from a normal clone  | You want modularity, opt-in modules, and man pages |

Pick whichever fits your taste both are shown below in full.

## Prerequisites

You'll be pushing your configuration to a private remote, so set up SSH first if you haven't. My companion guide walks through it end to end: [Git SSH Keys for GitHub, GitLab, and Bitbucket on Linux](/blog/post/git-ssh-keys-for-github-gitlab-and-bitbucket-on-linux). New to the shell in general? Start with [Introduction to Linux CLI](/blog/post/introduction-to-linux-cli).

Set your Git identity before committing anything, so the history is attributed correctly from the very first commit:

```shell title="Set global Git identity"
git config --global user.name 'YOUR_NAME'
git config --global user.email 'YOUR_EMAIL@EXAMPLE.COM'
```

## Strategy 1: The bare Git repository

The classic trick: initialize a **bare** repository in a discrete folder (`$HOME/.dotfiles`) and point its work-tree at `$HOME`. No symlinks, no copying your real home directory _becomes_ the work-tree.

### Initial setup

1. Create a bare Git repository in your home directory:

```shell title="Create a bare Git repository"
git init --bare $HOME/.dotfiles
```

2. Add a `config` alias to your shell profile so you can run Git against that repo from anywhere. Pick the tab for your shell:

```shell title="~/.bashrc"
alias config='git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'
```

```shell title="~/.zshrc"
alias config='git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'
```

3. Tell Git not to show every untracked file in `$HOME` (which would be thousands), so `config status` stays readable:

```shell title="Hide untracked files"
config config --local status.showUntrackedFiles no
```

With the `config` alias in place, you now drive your dotfiles with ordinary Git commands just type `config` where you'd normally type `git`.

### Adding and committing dotfiles

Version-controlling a file is exactly like a normal repo, using `config` instead of `git`:

```shell title="Add and commit your shell + editor config"
config add .vimrc .bashrc .zshrc
config commit -m 'Add shell and editor config'
```

Then publish to a private remote for safekeeping and easy sync. Use the **SSH** remote so you never type credentials:

```shell title="Push to a remote repository"
config remote add origin git@github.com:YOUR_USERNAME/dotfiles.git
config branch -M main
config push -u origin main
```

  Treat this repo like any other public-facing thing: **never commit secrets**.
  Keep API tokens, SSH private keys, and `.netrc`-style credentials out of it
  add them to a `.gitignore` (tracked in the repo) before your first push.

### Replicating your environment on a new machine

On a fresh box, add the `config` alias to your shell profile, then clone the repo as **bare** into `$HOME/.dotfiles`:

```shell title="Clone the dotfiles repository (bare)"
git clone --bare git@github.com:YOUR_USERNAME/dotfiles.git $HOME/.dotfiles
```

Now check the files out into `$HOME`. If files like `.bashrc` already exist, Git refuses to overwrite them this one-liner backs up any conflicts, then checks out cleanly:

```shell title="Check out into $HOME (backing up conflicts)"
mkdir -p .dotfiles-backup && \
config checkout 2>&1 | egrep "\s+\." | awk '{print $1}' | \
xargs -I{} mv {} .dotfiles-backup/{}
config checkout -f
config config --local status.showUntrackedFiles no
```

That's it your environment is restored, and `config status` is clean.

## Strategy 2: A modular repo + a bootstrap script

The bare-repo trick is elegant, but everything lives as one flat checkout. Once your config grows aliases, shell functions, plugins, per-app configs, even your own CLI tools you'll want **structure** and **opt-in modules**. The approach I actually run does exactly that: a normal Git repo of organized modules, symlinked into place by an idempotent `setup` script.

You can browse the real thing here: [github.com/MKAbuMattar/dotfiles](https://github.com/MKAbuMattar/dotfiles).

### Repository layout

- dotfiles/
  - .aliases/ per-tool alias modules (35 modules)
  - .utils/ per-tool shell function libraries (17 modules)
  - .plugins/ zsh plugins + Python CLI plugins
  - .zsh/ core zsh settings (options, completion, keybindings)
  - .config/ third-party app configs (kitty, btop, mpv, …)
  - .docs/ markdown man-page sources (the source of truth)
  - .man/ generated roff man pages
  - .scripts/ build & maintenance scripts
  - .agents/ Claude Code skills used to author this repo
  - **.zshrc** the entry point you opt modules in/out from
  - **setup** one-shot bootstrap (idempotent; safe to re-run)
  - README.md

Instead of dumping files in `$HOME`, each subtree is **symlinked** into `~/.config/.dotfiles`, and `~/.zshrc` is symlinked to the repo's `.zshrc`. The clone stays the single source of truth edit a file in the repo and your live config updates instantly, because it _is_ the same file.

### Quick start

1. Clone the repository somewhere stable (not directly in `$HOME`):

```shell title="Clone the dotfiles"
git clone git@github.com:MKAbuMattar/dotfiles.git ~/Work/dotfiles
cd ~/Work/dotfiles
```

2. Run the bootstrap script. It's idempotent re-running it detects existing correct symlinks and skips them, and prompts before overwriting anything else:

```shell title="Bootstrap the environment"
./setup
```

3. Reload your shell (or just open a new terminal):

```shell title="Reload zsh"
source ~/.zshrc
```

Here's roughly what a first run looks like:

_zsh_

### What the bootstrap does

1. **Symlinks** the repo (or its individual subtrees) into `~/.config/.dotfiles`.
2. **Links** `~/.zshrc` to the repo's `.zshrc`.
3. **Builds** a modular `~/.gitconfig` `[include]` block from every `*.gitconfig` shipped in `.config/gitconfig/`.
4. **Generates** the man pages from `.docs/` and refreshes the `apropos` index.
5. **Verifies** that `git` and `zsh` are installed, and **backs up** any pre-existing non-symlink target before replacing it.

  Because the bootstrap only ever creates symlinks and backs up conflicts before
  touching them, it's safe to re-run after every `git pull` no clobbering, no
  surprises.

### Opting modules in and out

The payoff of a modular layout: you choose what loads. The arrays in [`.zshrc`](https://github.com/MKAbuMattar/dotfiles/blob/main/.zshrc) are the control panel comment a line out and that module simply doesn't load:

```zsh title="~/.zshrc"
UTILS=("clipboard" "fedora" "git" "npm" "python")    # shell functions
PLUGINS=("aws" "docker" "fzf" "git" "kubectl")       # completion / integration
ALIASES=("docker" "exa" "general" "git" "npm")       # short command aliases
```

After editing, reload with Ctrl + C then `source ~/.zshrc`, or just open a new terminal.

### Self-documenting: man pages for everything

Every module ships an AWS-style markdown man page, compiled to real roff pages so `man <module>` and `apropos <keyword>` work for your own config exactly like they do for system tools:

_zsh_

### Built against reusable skills

The scripts in this repo aren't ad-hoc. The `setup` bootstrap and the Python CLI tools are written against two **agent skills** I maintain shared specs that encode strict Bash and Python patterns plus a validator, so quality stays consistent across machines and contributors. You can grab both for your own scripting from my skills repository: **[github.com/MKAbuMattar/skills](https://github.com/MKAbuMattar/skills)**.

- **`linux-script-developer`** strict Bash patterns + a validator the `setup` script passes at 100%.
- **`python-script-developer`** strict Python patterns + a validator every shipped Python plugin passes at 100%.

  Want the same guardrails on your own scripts? Pull the skills from
  [github.com/MKAbuMattar/skills](https://github.com/MKAbuMattar/skills) they're
  the exact specs (patterns + validators) the `setup` script and every Python
  plugin in the dotfiles are built and checked against.

If you fork the dotfiles and add a script, run the matching validator to keep it in line:

```shell title="Validate a new script against the skill"
bash    .agents/skills/linux-script-developer/scripts/validate-script.sh  ./your-script.sh
python3 .agents/skills/python-script-developer/scripts/validate-script.py ./your-script.py
```

  Once your shell is dialed in, give the terminal itself some polish a prompt,
  colors, icons. My guide on
  [customizing the terminal with Starship](/blog/post/customization-windows-terminal-with-starship)
  pairs perfectly with a modular dotfiles setup.

## Which should you choose?

> **I just want my configs versioned with zero fuss**

Use the **bare Git repository** (Strategy 1). There's nothing to install and nothing to symlink your home directory is the work-tree, and `config` is just `git` with a different `--git-dir`. It's the fastest path from "unmanaged" to "versioned and synced."

> **My config is large and I want opt-in modules**

Use the **modular repo + bootstrap** (Strategy 2). Splitting aliases, functions, plugins, and app configs into separate files keeps everything navigable, and a `setup` script makes a new machine reproducible in one command. The symlink model also means editing the repo updates your live config instantly.

> **Can I migrate from bare to modular later?**

Yes. Your files are already in Git either way. Move them into a structured layout, write (or borrow) a `setup` script that symlinks them, and point your shell at the new location. Nothing about the bare approach locks you in.

> **How do I keep secrets out of a public dotfiles repo?**

Never commit credentials. Keep a tracked `.gitignore` that excludes things like `~/.ssh/id_*`, `.netrc`, and any `*.env` files, and load real secrets from a separate, untracked file your shell sources only if it exists (e.g. `[[ -f ~/.secrets ]] && source ~/.secrets`).

## Conclusion

Whichever strategy you pick, the win is the same: your environment stops being a fragile, one-of-a-kind artifact and becomes something reproducible, reviewable, and one `git clone` away. Start with the bare repo if you want to be versioned in five minutes; graduate to a modular repo with a bootstrap script when your config earns the structure. Either way, the next machine you sign in to will already feel like home.

## References

- [The best way to store your dotfiles: A bare Git repository](https://www.atlassian.com/git/tutorials/dotfiles) (Atlassian)
- [MKAbuMattar/dotfiles the modular repo + bootstrap shown above](https://github.com/MKAbuMattar/dotfiles)
- [MKAbuMattar/skills the linux- and python-script-developer skills](https://github.com/MKAbuMattar/skills)
- [Hacker News Discussion on Dotfiles Management](https://news.ycombinator.com/item?id=11070797)
- [Managing Dotfiles With a Bare Git Repository](https://harfangk.github.io/2016/09/18/manage-dotfiles-with-a-git-bare-repository.html)
- [Dotfiles.github.io A guide to dotfiles on GitHub](https://dotfiles.github.io/)
- [Using Git and GitHub to manage your dotfiles](https://www.anishathalye.com/2014/08/03/managing-your-dotfiles/) (Anish Athalye)
- [Ask HN: How do you manage your dotfiles?](https://news.ycombinator.com/item?id=2509090)
- [Git Bare Repository A Better Way To Manage Dotfiles](https://driesvints.com/blog/using-a-git-bare-repository-to-manage-your-dotfiles/) (Dries Vints)
- [ArchWiki: Dotfiles](https://wiki.archlinux.org/title/Dotfiles)
- [GitHub Does Dotfiles](https://github.blog/2021-02-10-github-does-dotfiles/) (GitHub Blog)
- [GNU Stow for dotfiles management](https://www.gnu.org/software/stow/) (Alternative approach)
- [Chezmoi Manage your dotfiles across multiple diverse machines, securely](https://www.chezmoi.io/) (Popular dotfiles manager tool)
- [YADM Yet Another Dotfiles Manager](https://yadm.io/) (Another popular tool)
- [Understanding Git --bare repositories](https://www.saintsjd.com/2011/01/what-is-a-bare-git-repository/)
- [Stack Overflow: What is a bare git repository?](https://stackoverflow.com/questions/219977/what-is-a-bare-git-repository)
