Setting up a new development machine from scratch takes hours if you do it manually — reconfiguring your shell, reinstalling tools, recreating aliases, copying settings. Dotfiles fix this. Your configuration files (~/.zshrc, ~/.gitconfig, ~/.vimrc, VS Code settings) live in a git repository, and symlinks connect them to where the tools expect them. When you get a new machine, you clone the repo and run one script. This guide shows a practical setup without over-engineering it.

What Are Dotfiles?

“Dotfiles” is the informal name for configuration files that start with a dot (.) — Unix’s convention for hidden files. They live in your home directory and control the behavior of your shell, editor, git, and other tools.

Common ones:

~/.zshrc          Zsh shell configuration (aliases, exports, plugins)
~/.bashrc         Bash equivalent
~/.gitconfig      Git user info, aliases, and defaults
~/.vimrc          Vim configuration
~/.tmux.conf      Tmux key bindings and appearance
~/.ssh/config     SSH host aliases and connection settings

The insight is that these files are just text — they belong in version control like any other source code.

The standard pattern: keep all your dotfiles in a ~/dotfiles directory, then symlink each one to its expected location in ~.

~/dotfiles/
    zshrc         ← actual file
    gitconfig     ← actual file
    vimrc         ← actual file
    tmux.conf     ← actual file

~/.zshrc          → ~/dotfiles/zshrc     (symlink)
~/.gitconfig      → ~/dotfiles/gitconfig (symlink)
~/.vimrc          → ~/dotfiles/vimrc     (symlink)
~/.tmux.conf      → ~/dotfiles/tmux.conf (symlink)

When a tool reads ~/.zshrc, it follows the symlink and reads from your git-tracked file. Any change you make is immediately tracked.

Step 1: Create the Dotfiles Repository

$ mkdir ~/dotfiles && cd ~/dotfiles
$ git init
$ git remote add origin git@github.com:yourusername/dotfiles.git

Move your existing config files in:

$ mv ~/.zshrc ~/dotfiles/zshrc
$ mv ~/.gitconfig ~/dotfiles/gitconfig
$ mv ~/.vimrc ~/dotfiles/vimrc       # if you use vim
$ mv ~/.tmux.conf ~/dotfiles/tmux.conf  # if you use tmux

Create symlinks back to the expected locations:

$ ln -sf ~/dotfiles/zshrc ~/.zshrc
$ ln -sf ~/dotfiles/gitconfig ~/.gitconfig
$ ln -sf ~/dotfiles/vimrc ~/.vimrc
$ ln -sf ~/dotfiles/tmux.conf ~/.tmux.conf

Verify the symlinks:

$ ls -la ~ | grep "\->"
lrwxr-xr-x  .gitconfig -> /Users/mukulkadel/dotfiles/gitconfig
lrwxr-xr-x  .tmux.conf -> /Users/mukulkadel/dotfiles/tmux.conf
lrwxr-xr-x  .vimrc     -> /Users/mukulkadel/dotfiles/vimrc
lrwxr-xr-x  .zshrc     -> /Users/mukulkadel/dotfiles/zshrc

Commit and push:

$ cd ~/dotfiles
$ git add .
$ git commit -m "Initial dotfiles"
$ git push -u origin main

Step 2: Write an Install Script

Manual symlinking works for the first setup, but an install script makes restoration on a new machine a single command. Create ~/dotfiles/install.sh:

#!/usr/bin/env bash
set -e

DOTFILES="$HOME/dotfiles"

link() {
    local src="$DOTFILES/$1"
    local dst="$HOME/.$1"

    if [ -e "$dst" ] && [ ! -L "$dst" ]; then
        echo "Backing up existing $dst → $dst.bak"
        mv "$dst" "$dst.bak"
    fi

    ln -sf "$src" "$dst"
    echo "Linked: $dst → $src"
}

link zshrc
link gitconfig
link vimrc
link tmux.conf
link ssh/config

echo "Done."
$ chmod +x ~/dotfiles/install.sh

The if [ -e "$dst" ] && [ ! -L "$dst" ] check backs up any existing file before overwriting — so you don’t lose configuration that’s already on a machine when you run the script.

Step 3: Add a Brewfile

Pair your dotfiles with a Brewfile to capture all installed packages:

$ brew bundle dump --file=~/dotfiles/Brewfile

Add to your install script:

if command -v brew &>/dev/null; then
    brew bundle install --file="$DOTFILES/Brewfile"
fi

Handling Machine-Specific Configuration

Some settings differ between machines (work vs. personal, Mac vs. Linux). The cleanest pattern is a local override file that isn’t committed:

In ~/dotfiles/zshrc:

# Load machine-local overrides if present
[ -f ~/.zshrc.local ] && source ~/.zshrc.local

On a work machine, ~/.zshrc.local might set:

export WORK_API_KEY="..."
export JAVA_HOME="/opt/homebrew/opt/openjdk@17"
alias vpn="open -a 'Cisco AnyConnect'"

This file never goes into the repo (add *.local to .gitignore), so secrets and machine-specific paths stay off GitHub.

SSH Config in Dotfiles

Your ~/.ssh/config is safe to version-control as long as it doesn’t contain private keys (those live in ~/.ssh/ but are never committed). A typical config:

# ~/.ssh/config
Host github.com
    User git
    IdentityFile ~/.ssh/id_ed25519
    AddKeysToAgent yes

Host work-server
    HostName 10.0.1.42
    User deploy
    IdentityFile ~/.ssh/work_ed25519
    ForwardAgent yes

Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3

Symlink it:

$ mkdir -p ~/dotfiles/ssh
$ mv ~/.ssh/config ~/dotfiles/ssh/config
$ ln -sf ~/dotfiles/ssh/config ~/.ssh/config

Setting Up a New Machine

With the repo in place, getting a new Mac developer-ready is:

# Install Homebrew
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Clone dotfiles
$ git clone git@github.com:yourusername/dotfiles.git ~/dotfiles

# Run install script (links dotfiles + installs Brewfile packages)
$ ~/dotfiles/install.sh

# Reload shell
$ source ~/.zshrc

From bare macOS to a fully configured environment in under 10 minutes, without touching the mouse.

What to Keep Out of Dotfiles

A few things that look like good candidates but aren’t:

  • Private keys (~/.ssh/id_ed25519) — never commit these. Use 1Password SSH agent or similar.
  • Tokens and secrets — put these in ~/.zshrc.local or a secrets manager.
  • IDE-specific binary caches — VS Code’s ~/.vscode extension installations are machine-specific; just commit extensions.json recommendations instead.

Conclusion

A dotfiles repo costs about 30 minutes to set up the first time and pays for itself on the first new machine setup. The symlink approach is the right primitive — your tools read from their expected paths, your config lives in git, and the relationship is transparent. Keep the install script simple, use a local override file for machine-specific secrets and paths, and add a Brewfile so package installation is just as automated.