Every "set up a new Mac" checklist eventually rebuilds the same shell prompt, Git config, and editor settings from memory — a dotfiles repo turns that into git clone plus one script.
- A GitHub (or similar) account to host the dotfiles repository.
- Existing configuration files worth preserving —
.zshrc,.gitconfig, and similar.
Step 1: Create the dotfiles repository
mkdir ~/dotfiles && cd ~/dotfiles
git init
git remote add origin https://github.com/you/dotfiles.gitA flat structure works fine for most individual setups — one file per config, named after what it configures:
dotfiles/
.zshrc
.gitconfig
.vimrc
starship.toml
Step 2: Move real config into the repo, then symlink it back
mv ~/.zshrc ~/dotfiles/.zshrc
ln -s ~/dotfiles/.zshrc ~/.zshrcln -s <target> <link> creates a symbolic link — ~/.zshrc becomes a pointer to ~/dotfiles/.zshrc, not a separate copy. Editing ~/.zshrc directly (the symlink) or ~/dotfiles/.zshrc (the real file) both edit the exact same underlying file.
Step 3: Repeat for each config file, then commit
mv ~/.gitconfig ~/dotfiles/.gitconfig
ln -s ~/dotfiles/.gitconfig ~/.gitconfig
cd ~/dotfiles
git add .
git commit -m "Initial dotfiles"
git push -u origin mainStep 4: A setup script for future machines
The actual payoff — a script that recreates every symlink automatically, so a new machine setup becomes git clone plus one command instead of manually remembering each ln -s:
#!/bin/sh
# install.sh
DOTFILES_DIR="$HOME/dotfiles"
for file in .zshrc .gitconfig .vimrc; do
target="$HOME/$file"
if [ -e "$target" ] && [ ! -L "$target" ]; then
mv "$target" "$target.backup"
fi
ln -sf "$DOTFILES_DIR/$file" "$target"
done
echo "Dotfiles linked."git clone https://github.com/you/dotfiles.git ~/dotfiles
~/dotfiles/install.shThe [ -e "$target" ] && [ ! -L "$target" ] check backs up an existing real file before symlinking over it, but skips the backup if it's already a symlink (meaning the script has run before) — safe to re-run without accumulating .backup files on every execution.
Handling machine-specific differences
Not every setting should be identical across machines (a work laptop vs. a personal one might need different Git identities). The common pattern: a shared base file plus a small, untracked local override, sourced conditionally:
# .gitconfig (tracked, shared)
[include]
path = ~/.gitconfig.local# ~/.gitconfig.local (untracked, machine-specific — in .gitignore)
[user]
email = work-email@company.comThis keeps the shared config in version control while letting each machine layer its own local, untracked differences on top without polluting the shared repo.
What to keep out of a public dotfiles repo
SSH private keys, API tokens, and any file containing real credentials should never be committed, even to a private repo — treat a dotfiles repo the same as any other codebase for secrets hygiene. A .gitignore covering common sensitive patterns (*.pem, .netrc, *_rsa) is worth setting up from the very first commit, not retrofitted after something sensitive is accidentally already in the history.
Using a dedicated dotfiles manager instead of raw symlinks
For a larger dotfiles collection, tools like GNU Stow or chezmoi automate exactly the symlinking process described above, handling edge cases (conflicting existing files, per-machine templating) more robustly than a hand-written script:
brew install stow
cd ~/dotfiles
stow zsh git vim # symlinks every file in each named package into $HOMEStow organizes dotfiles into named "packages" (a directory per tool) and symlinks their contents into your home directory with one command per package — worth adopting once a hand-rolled install.sh starts accumulating enough special cases to feel like it's reinventing what these tools already solve well.
Verifying symlinks are actually working
After running a setup script on a new machine, confirming the links resolved correctly is worth doing explicitly rather than assuming:
ls -la ~ | grep "\->"
# .zshrc -> /Users/you/dotfiles/.zshrcls -la shows symlinks with a -> pointing at their target — if a file that should be a symlink instead shows as a regular file, the setup script didn't run correctly (often because the file already existed and the backup-then-link step was skipped or failed silently).
Bootstrapping Homebrew packages alongside dotfiles
A Brewfile committed to the same dotfiles repo extends this "clone and go" setup to actual installed packages, not just config files:
# ~/dotfiles/Brewfile
brew "git"
brew "starship"
cask "visual-studio-code"brew bundle --file ~/dotfiles/BrewfileAdding this single line to the install.sh script from earlier means a fresh machine gets both the real configuration and the tools that configuration assumes exist, from one script — the same "clone and run one command" experience extended from just dotfiles to the full toolchain.
Common mistakes
- Copying files into the dotfiles repo instead of moving them and symlinking back — this creates two independent copies that silently drift apart the moment either one is edited without updating the other.
- Committing a real SSH private key or API token because it happened to live in a config file being tracked. Once in git history, it's effectively permanent — audit exactly what's being added before the first commit, not after.
- Overwriting an existing config file with
ln -sfwithout backing it up first, losing machine-specific settings that were never in the dotfiles repo to begin with. - Hardcoding a machine-specific value (a work email, a local path) directly into the shared, tracked config instead of using an untracked local-override file — this breaks the moment the same dotfiles are cloned onto a different machine.
Related reading
- Homebrew Explained: The macOS Package Manager for Developers — shares tags: macos, productivity (same category).
- Setting Up a macOS Development Environment from Scratch — shares tags: macos, productivity.
- Git Hooks Explained: Automate Your Workflow — shares tags: productivity.
- Understanding File Permissions in Linux (chmod, chown Explained) — shares tags: productivity.
- Windows Terminal: Customization Tips for Developers — shares tags: productivity.