Personal dotfiles managed with GNU Stow.
Cross-platform (macOS + Linux): on macOS, Brewfile drives dependency
install; on Linux, tools are managed out-of-band (distro package manager /
cargo / manual) and the bootstrap only runs stow.
Each top-level directory is a stow package that mirrors the structure
relative to $HOME (except scripts/, which holds repo helpers and is not
stowed). Stowing a package symlinks its contents into the matching path
under $HOME.
dotfiles/
├── .stowrc # portable stow defaults (--no-folding, --verbose=1)
├── Brewfile # tool dependencies for `brew bundle`
├── Makefile # day-to-day interface (`make help`)
├── install.sh # one-shot bootstrap (brew bundle + stow everything)
├── aerospace/.config/aerospace/aerospace.toml
├── bat/.config/bat/{config,themes/}
├── btop/.config/btop/{btop.conf,themes/}
├── claude/.claude/{settings.json,CLAUDE.md,statusline.sh,hooks/,skills/}
├── eza/.config/eza/theme.yml
├── fastfetch/.config/fastfetch/{config.jsonc,logo.txt}
├── gh/.config/gh/config.yml # hosts.yml is intentionally NOT tracked
├── ghostty/.config/ghostty/{config,themes/}
├── lazygit/.config/lazygit/config.yml
├── nvim/.config/nvim/{init.lua,lua/,lazy-lock.json}
├── sketchybar/.config/sketchybar/{sketchybarrc,colors.sh,icon_map.sh,plugins/}
├── scripts/ # repo helpers (NOT a stow package)
├── git/.gitconfig # top-level dotfile
└── zsh/{.zshrc,.zprofile} # top-level dotfiles
stow aerospace creates the symlink
~/.config/aerospace/aerospace.toml -> ~/Developer/dotfiles/aerospace/.config/aerospace/aerospace.toml
git clone <this-repo> ~/Developer/dotfiles
cd ~/Developer/dotfiles
# 1. One command: brew bundle + symlink every package into $HOME.
make install
# 2. Drop personal info into the gitignored *.local files
# (see "Per-machine overrides" below).On macOS, make install runs brew bundle --file=Brewfile and then re-stows
every package. On Linux it skips Homebrew and only re-stows (install the tools
the configs reference yourself). If you don't have make (or just prefer the
script), ./install.sh does the same thing — it detects the OS the same way.
The Makefile is the friendly interface. Run make help to list targets.
| Target | What it does |
|---|---|
make / make help |
Print all targets |
make install |
Full bootstrap: brew bundle + restow everything |
make brew |
Install/refresh Homebrew deps from Brewfile |
make restow |
Re-symlink every package (idempotent) |
make stow PKG=nvim |
Stow a single package |
make unstow PKG=nvim |
Unstow a single package |
make check |
Dry-run stow + brew bundle check |
make status |
Show discovered packages, target dir |
make clean |
Unstow every package |
Both install.sh and the Makefile pass --target=$HOME and --dir=<repo>
explicitly. .stowrc only carries --no-folding and --verbose=1.
- Create the package mirror:
mkdir -p newtool/.config/newtool. - Move the live config in:
mv ~/.config/newtool/* newtool/.config/newtool/. - Stow it:
stow newtool. - Commit.
Stow packages can mirror any path relative to $HOME, not just .config/.
The zsh/ package is the example — it puts files at the package root so
they land directly in $HOME:
zsh/
├── .zshrc # → ~/.zshrc
└── .zprofile # → ~/.zprofile
Then stow zsh. Same pattern works for .gitconfig, .tmux.conf, etc.
Personal identity, secrets, and host-specific tweaks don't belong in a shared dotfiles repo. The configs here source local override files if they exist, all of which are gitignored:
| Config | Override file | Loaded via |
|---|---|---|
git/.gitconfig |
~/.gitconfig.local |
[include] path = ~/.gitconfig.local |
zsh/.zshrc |
~/.zshrc.local |
if [[ -f ~/.zshrc.local ]]; then source ~/.zshrc.local; fi |
zsh/.zprofile |
~/.zprofile.local |
same pattern |
Use these for things like:
~/.gitconfig.local—[user]block (name, email, signingkey),commit.gpgSign, work-specificincludeIfpaths, andcredential.helper(per-machine:osxkeychainon macOS,libsecreton Linux, orcache --timeout=<secs>as a portable fallback — git has no OS conditional)~/.zshrc.local— private aliases, API keys exported as env vars, per-machine PATH entries~/.zprofile.local— login-shell-only secrets or host setup
If the override file doesn't exist, the parent config is a no-op for that section. New forkers get a working baseline and add their own identity/secrets without ever touching the tracked files.
Every brew formula and cask the configs in this repo depend on is declared
in Brewfile at the repo root. To install or refresh them:
brew bundle --file=Brewfile # install everything that's missing
brew bundle check --file=Brewfile # show what's not installed yet
brew bundle cleanup --file=Brewfile --force # uninstall things not in BrewfileA few things aren't brew-managed and are installed separately on a fresh
machine (run these after brew bundle):
- oh-my-zsh + plugins (
zsh-autosuggestions,zsh-syntax-highlighting) referenced byzsh/.zshrc - nvm (Node version manager) — used by
.zshrcfor theload-nvmrchook - sdkman — used by
.zshrcand.zprofilefor JVM tooling
gh/hosts.ymlis intentionally excluded — it contains auth tokens. It stays in~/.config/gh/as a real file and stow only linksconfig.yml.git/.gitconfigis identity-free. Personal[user],commit.gpgSign, and per-clientincludeIfdirectives live in~/.gitconfig.local(gitignored). See the "Per-machine overrides" section above.--no-foldingis enabled so each file becomes its own symlink (rather than letting stow collapse a whole directory into one symlink). This makes it safe for tools that write extra runtime files into~/.config/<tool>/.yazi/is currently dropped from the repo (its config is being reworked). Add it back as a stow package when ready.baton Debian/Ubuntu is packaged asbatcat(binary-name clash). The configs callbat(git pager via delta, fzf preview,MANPAGER); create a shim so the name resolves:ln -s "$(command -v batcat)" ~/.local/bin/bat.MANPAGERalso falls back tobatcatautomatically if only that exists.- macOS-only tooling (AeroSpace, sketchybar, OrbStack, the
sketchybar-icon-maptarget) is guarded to no-op on Linux, so a single repo serves both machines.