Dotfiles, defined: what goes into a development machine
A dotfiles repository is the single source of truth for your personal development environment. It captures your tools, their configurations, your conventions, and the bootstrap scripts that reproduce them on a fresh machine.
-
Purpose
- Reproducibly set up a machine from nothing to productive in under an hour.
- Keep your preferences portable across macOS, Linux, and cloud dev environments.
- Document decisions so future-you can remember why.
-
Scope (what belongs in dotfiles)
- Terminals, shells, prompts, and multiplexers.
- Editors and their settings, extensions, and language tooling.
- Package manager definitions (Brewfile, asdf/mise, Nix/Home Manager if used).
- Aliases, functions, and helper scripts in
~/bin. - Program configuration in
~/.config(XDG Base Directory). - Git configuration and signing setup.
- Window management (macOS: AeroSpace) and system-tuning scripts.
-
Out of scope
- Secrets (API keys, tokens) — manage via 1Password CLI,
sops, orage, templated at apply-time. - Heavy system imaging (that’s for MDMs or golden images).
- Secrets (API keys, tokens) — manage via 1Password CLI,
Principles (opinionated)
- Idempotent bootstrap
- Re-running the bootstrap should converge to the same state without breaking.
- XDG-first
- Prefer
~/.config/<app>and export XDG env vars so configs are predictable and greppable.
- Prefer
- Portability with graceful degradation
- macOS-first examples with Linux fallbacks; avoid platform-specific shellisms when possible.
- Small core, layers on top
- Start minimal; layer optional stacks (e.g., language toolchains) via
make features/<name>.
- Start minimal; layer optional stacks (e.g., language toolchains) via
- Declarative installs
- Use Brewfile and a tool like
mise(orasdf) for language/runtime versions.
- Use Brewfile and a tool like
- Fast feedback
- Use a throwaway VM/Container to test the bootstrap on every change.
Architecture of a dotfiles repo
- Repo layout (example)
~/.dotfiles/
├─ Brewfile
├─ README.md
├─ Makefile
├─ bin/
│ └─ (helpers: backup, macos-defaults, git-helpers)
├─ bootstrap/
│ ├─ macos.sh
│ ├─ linux.sh
│ └─ common.sh
├─ config/ # maps to $XDG_CONFIG_HOME
│ ├─ wezterm/wezterm.lua
│ ├─ ghostty/config
│ ├─ kitty/kitty.conf
│ ├─ nvim/
│ ├─ tmux/tmux.conf
│ ├─ zellij/config.kdl
│ ├─ starship.toml
│ └─ aerospace/
├─ home/ # files that live in $HOME
│ ├─ .gitconfig
│ ├─ .gitignore_global
│ ├─ .zshrc (thin wrapper -> $XDG_CONFIG_HOME/zsh/.zshrc)
│ └─ .inputrc
├─ shells/
│ ├─ zsh/
│ │ ├─ .zshenv
│ │ ├─ .zshrc
│ │ └─ completions/
│ └─ fish/config.fish
├─ features/
│ ├─ node.mise.toml
│ ├─ python.mise.toml
│ └─ rust.mise.toml
└─ templates/ # secrets-aware templates (chezmoi/dotbot users)- Management strategies
- chezmoi: batteries included, cross-platform, secrets templating. Great default.
- GNU stow + Makefile: simple symlinking; you fully control ordering/logic.
- yadm: git with bootstrap hooks; minimal ceremony.
- Nix + Home Manager: fully declarative; steeper learning curve; superb reproducibility.
- Choose one. Don’t mix.
Bootstrap flow (macOS example)
-
0) Prereqs
- Install Xcode CLI tools:
xcode-select --install - (Apple Silicon) Rosetta, if needed:
softwareupdate --install-rosetta --agree-to-license
- Install Xcode CLI tools:
-
1) Homebrew and base CLI
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew update
brew analytics off
brew tap homebrew/cask-fonts- 2) Clone dotfiles and apply
# choose your strategy
# chezmoi (recommended)
brew install chezmoi
chezmoi init <your-github-username> --apply
# or stow
git clone https://github.com/<you>/dotfiles ~/.dotfiles
cd ~/.dotfiles && make bootstrap # wraps stow + brew bundle- 3) Brewfile (apps and fonts)
# Brewfile (excerpt)
brew "git"
brew "zsh"
brew "ripgrep"
brew "fd"
brew "fzf"
brew "bat"
brew "eza"
brew "zoxide"
brew "git-delta"
brew "gh"
brew "starship"
brew "tmux"
brew "zellij"
cask "wezterm"
cask "ghostty"
cask "kitty"
cask "iterm2"
cask "font-jetbrains-mono-nerd-font"
# AeroSpace is distributed via a tap; see the project docs for the latest tap/cask name- 4) Language/runtime managers
- Prefer
mise(fast, unified) orasdf(mature ecosystem).
- Prefer
brew install mise # or: brew install asdf
mise use -g node@lts [email protected] rust@stable
mise doctor- 5) Validate
- Open terminal; confirm prompt, keybindings, and tools exist.
- Run
make doctorto check versions, paths, and config locations.
Terminal: Kitty, Ghostty, iTerm2, WezTerm (choose one or two)
-
Selection guidance
- WezTerm: GPU-accelerated, Lua config, multiplexing-over-SSH, great font shaping.
- Ghostty: very fast on macOS, modern config, early-stage but promising.
- Kitty: performant, keyboard-driven, distributed config.
- iTerm2: mature macOS classic, feature-rich GUI preferences.
-
Config locations
- WezTerm:
$XDG_CONFIG_HOME/wezterm/wezterm.lua(or~/.wezterm.lua). - Ghostty:
$XDG_CONFIG_HOME/ghostty/config. - Kitty:
$XDG_CONFIG_HOME/kitty/kitty.conf. - iTerm2: export a Preferences profile; optional Dynamic Profiles JSON under
~/Library/Application Support/iTerm2/DynamicProfiles.
- WezTerm:
-
Baseline settings to unify across terminals
- Fonts: Nerd Font, e.g., JetBrainsMono Nerd Font.
- Keybinds: copy-on-select, Option as Meta, natural text selection, predictable tabs.
- Theme: a single palette (e.g., Catppuccin, Tokyo Night) shared across editor/terminal.
-- $XDG_CONFIG_HOME/wezterm/wezterm.lua (minimal)
local wezterm = require 'wezterm'
return {
color_scheme = "Catppuccin Mocha",
font = wezterm.font_with_fallback({"JetBrainsMono Nerd Font", "SF Mono"}),
font_size = 13.0,
enable_scroll_bar = false,
}Shell: zsh, fish, bash
-
Choices
- zsh (macOS default): rich ecosystem, good completion; pair with a plugin manager.
- fish: great UX out-of-the-box; pair with
fisherfor plugins. - bash: everywhere; keep a minimal
.bashrcfor remote machines.
-
Prompt
- Use
starshipfor a consistent, fast, cross-shell prompt.
- Use
-
zsh skeleton
# ~/.zshenv: set XDG early
export XDG_CONFIG_HOME="$HOME/.config"
export XDG_CACHE_HOME="$HOME/.cache"
export XDG_DATA_HOME="$HOME/.local/share"
export XDG_STATE_HOME="$HOME/.local/state"
# ~/.zshrc: plugins + PATH
autoload -Uz compinit && compinit
export PATH="/opt/homebrew/bin:$HOME/.local/bin:$PATH"
# plugin manager (example: zinit)
if [[ -f "$HOME/.local/share/zinit/zinit.git/zinit.zsh" ]]; then
source "$HOME/.local/share/zinit/zinit.git/zinit.zsh"
fi
zinit light zsh-users/zsh-autosuggestions
zinit light zsh-users/zsh-completions
zinit light zsh-users/zsh-syntax-highlighting
# prompt
if command -v starship >/dev/null 2>&1; then
eval "$(starship init zsh)"
fi
# fzf keybindings (installed via brew)
[[ -f "$(brew --prefix)"/opt/fzf/shell/key-bindings.zsh ]] && \
source "$(brew --prefix)"/opt/fzf/shell/key-bindings.zsh- fish skeleton
# ~/.config/fish/config.fish
set -x XDG_CONFIG_HOME $HOME/.config
set -x PATH /opt/homebrew/bin $HOME/.local/bin $PATH
if type -q starship
starship init fish | source
endEditor: Vim/Neovim, VSCode/Windsurf, Emacs
- Neovim
- Keep
~/.config/nvimclean. Uselazy.nvimfor plugin management. - Essentials: Treesitter, LSP, Telescope, statusline, formatter, linter.
- Keep
-- ~/.config/nvim/init.lua (skeleton)
require('lazy').setup({
{ 'nvim-treesitter/nvim-treesitter', build = ':TSUpdate' },
{ 'neovim/nvim-lspconfig' },
{ 'nvim-telescope/telescope.nvim' },
{ 'nvim-lualine/lualine.nvim' },
})- VSCode / Windsurf
- Sync
settings.json,keybindings.json, andextensions.jsonunder$XDG_CONFIG_HOME/Code/User/.
- Sync
// settings.json (excerpt)
{
"editor.formatOnSave": true,
"terminal.integrated.fontFamily": "JetBrainsMono Nerd Font",
"files.trimTrailingWhitespace": true,
"workbench.colorTheme": "Catppuccin Mocha"
}# extensions (sample)
code --install-extension ms-python.python
code --install-extension ms-vscode.cpptools
code --install-extension rust-lang.rust-analyzer
code --install-extension esbenp.prettier-vscode
code --install-extension eamodio.gitlens- Emacs
- Consider Doom Emacs (fast starter) and sync
~/.config/doom.
- Consider Doom Emacs (fast starter) and sync
Tools and programs to install (CLI + GUI)
-
Command-line
ripgrep,fd,fzf,bat,eza,zoxide,direnv,jq,yq,gh,git-delta,btop/htop,tree,watch,entr.- Language:
miseorasdf; compilers as needed (llvm,gcc),pkg-config. - Security:
gpg,pinentry-mac,age,sops,1password-cli.
-
GUI apps (macOS)
- Terminals: WezTerm, Ghostty, Kitty, iTerm2.
- Browser: Firefox Developer Edition, Safari, Arc, or Chrome (choose one default).
- WM and input: AeroSpace, Karabiner-Elements (optional), Rectangle (fallback tiling).
Shortcuts and aliases (ergonomics)
-
Filesystem
alias ll='eza -l --git'alias la='eza -la --git'mkcd() { mkdir -p "$1" && cd "$1"; }
-
Git
alias gs='git status -sb'alias gl='git --no-pager log --oneline --graph --decorate -n 20'alias gp='git push',alias gco='git checkout',alias ga='git add -A'
-
Search/navigation
fzfbindings,zoxide(z footo jump),rgdefaults via.ripgreprc.
Keep aliases minimal; prefer scripts in ~/bin for anything >1 line.
Program config: XDG Base Directory
-
What it is
- A convention to store configs in predictable places:
~/.config(config),~/.local/share(data),~/.cache(cache),~/.local/state(state).
- A convention to store configs in predictable places:
-
Exports to add early
export XDG_CONFIG_HOME="$HOME/.config"
export XDG_DATA_HOME="$HOME/.local/share"
export XDG_CACHE_HOME="$HOME/.cache"
export XDG_STATE_HOME="$HOME/.local/state"-
Examples
~/.config/wezterm/wezterm.lua~/.config/nvim/init.lua~/.config/tmux/tmux.confand~/.config/zellij/config.kdl~/.config/starship.toml
-
Zsh gotcha
- If you relocate zsh configs to XDG, set
ZDOTDIR="$XDG_CONFIG_HOME/zsh"in~/.zshenvand keep it in$HOME.
- If you relocate zsh configs to XDG, set
Git config (portable and secure)
- Global config
# ~/.gitconfig
[user]
name = Your Name
email = [email protected]
signingkey = ABCE1234
[commit]
gpgsign = true
[gpg]
program = gpg
[core]
editor = nvim
excludesfile = ~/.gitignore_global
[pull]
rebase = false
[push]
default = current
[pager]
log = delta
diff = delta
reflog = delta
[delta]
features = decorations
navigate = true
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work-
SSH vs GPG signing
- Modern Git and GitHub support SSH signing; consider it if you already manage SSH keys.
-
Commit hygiene
- Use a
~/.gitmessagetemplate; setcommit.templateto point to it.
- Use a
Browser (developer profile)
- Choose one default: Firefox Dev Edition or Chrome-based. Safari is fine but limited in dev extensions.
- Extensions to note
- uBlock Origin, React DevTools, Redux DevTools, JSON Viewer, Wappalyzer, 1Password, Dark Reader.
- Profiles
- Separate work and personal profiles; sync is outside dotfiles; keep a list in README.
Terminal multiplexer: tmux or zellij
- tmux (classic)
# ~/.config/tmux/tmux.conf (excerpt)
set -g mouse on
set -g history-limit 100000
set -g status-interval 5
set -g default-terminal "tmux-256color"
bind -n C-Space last-window- zellij (modern)
// ~/.config/zellij/config.kdl (excerpt)
keybinds {
normal {
bind "Ctrl g" { SwitchToMode "locked" }
}
}Pick one to avoid muscle-memory conflicts.
Window manager (AeroSpace on macOS)
-
Why
- Keyboard-driven tiling, fast workspace switching, consistent layouts across machines.
-
Install
- Install via the project’s Homebrew tap (refer to AeroSpace docs for the current tap/cask).
-
Configure
- Put config under
$XDG_CONFIG_HOME/aerospace/. - Map
Alt/Option + H/J/K/Lto focus,Alt + Shift + H/J/K/Lto move. - Define workspaces per role (1: code, 2: web, 3: docs/notes, 4: comms).
- Put config under
Secrets management (don’t commit secrets)
- 1Password CLI to inject tokens at apply-time.
- age/sops to encrypt small files; decrypt on-demand via
make secrets/decrypt. - chezmoi templates can conditionally render secrets if present.
Testing and CI for dotfiles
- Local fast loop
- Use a disposable container/VM to test bootstrap scripts.
docker run --rm -it ubuntu:24.04 bash- CI
- Run
shellcheckon scripts,styluaon Lua configs,actionlinton workflows. - Optionally run the bootstrap in a GitHub Action with a Linux runner.
- Run
Onboarding and maintenance
- README
- Document the one-liner bootstrap, prerequisites, and a high-level map of the repo.
- Makefile targets
make bootstrap,make update,make doctor,make features/<stack>.
- Backups
- Exclude
~/.sshand secrets; back up via your password manager or secure vault.
- Exclude
Quickstart checklist (macOS)
- Install Xcode CLI tools
- Install Homebrew
-
brew bundlewith your Brewfile - Install
chezmoior set upstow+make - Apply dotfiles (creates symlinks into
$HOMEand$XDG_CONFIG_HOME) - Launch terminal (WezTerm/Ghostty) and verify prompt and keybinds
- Open editor (Neovim/VSCode) and verify language servers/extensions
- Set up Git signing and test
git commit - Configure AeroSpace and verify workspace keybindings