macOSBeginner

macOS Dotfiles and Symlinks: Managing Your Configuration Like Code

A step-by-step approach to keeping your shell, Git, and editor configuration in a version-controlled dotfiles repo, symlinked into place instead of scattered and unbacked-up.

DevFieldGuideJune 22, 2026 (updated June 24, 2026)6 min read
Share:

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.

Prerequisites
  • 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

bash
mkdir ~/dotfiles && cd ~/dotfiles
git init
git remote add origin https://github.com/you/dotfiles.git

A flat structure works fine for most individual setups — one file per config, named after what it configures:

dotfiles/ .zshrc .gitconfig .vimrc starship.toml
bash
mv ~/.zshrc ~/dotfiles/.zshrc
ln -s ~/dotfiles/.zshrc ~/.zshrc

ln -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.

~/dotfiles/.zshrcThe real file, tracked in git
~/.zshrcA symlink pointing at it
Shell reads ~/.zshrcTransparently follows the symlink

Step 3: Repeat for each config file, then commit

bash
mv ~/.gitconfig ~/dotfiles/.gitconfig
ln -s ~/dotfiles/.gitconfig ~/.gitconfig
 
cd ~/dotfiles
git add .
git commit -m "Initial dotfiles"
git push -u origin main

Step 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:

bash
#!/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."
bash
git clone https://github.com/you/dotfiles.git ~/dotfiles
~/dotfiles/install.sh

The [ -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:

bash
# .gitconfig (tracked, shared)
[include]
  path = ~/.gitconfig.local
bash
# ~/.gitconfig.local (untracked, machine-specific — in .gitignore)
[user]
  email = work-email@company.com

This 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.

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:

bash
brew install stow
cd ~/dotfiles
stow zsh git vim   # symlinks every file in each named package into $HOME

Stow 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.

After running a setup script on a new machine, confirming the links resolved correctly is worth doing explicitly rather than assuming:

bash
ls -la ~ | grep "\->"
# .zshrc -> /Users/you/dotfiles/.zshrc

ls -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:

ruby
# ~/dotfiles/Brewfile
brew "git"
brew "starship"
cask "visual-studio-code"
bash
brew bundle --file ~/dotfiles/Brewfile

Adding 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

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 -sf without 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.

Frequently Asked Questions

DevFieldGuide
DevFieldGuide

Editorial Team

Practical tutorials and developer tools, written and maintained by the DevFieldGuide team.

Enjoyed this article?

Get the next one straight to your inbox, along with the best of what we publish each week.

Related Articles

More in macOS

View all