skip to content

Search

Dotfiles Essentials

8 min read Updated:

What are the essential elements of setting up a dotfiles repository

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, or age, templated at apply-time.
    • Heavy system imaging (that’s for MDMs or golden images).

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.
  • 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>.
  • Declarative installs
    • Use Brewfile and a tool like mise (or asdf) for language/runtime versions.
  • 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
  • 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) or asdf (mature ecosystem).
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 doctor to 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.
  • 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 fisher for plugins.
    • bash: everywhere; keep a minimal .bashrc for remote machines.
  • Prompt

    • Use starship for a consistent, fast, cross-shell prompt.
  • 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
end

Editor: Vim/Neovim, VSCode/Windsurf, Emacs

  • Neovim
    • Keep ~/.config/nvim clean. Use lazy.nvim for plugin management.
    • Essentials: Treesitter, LSP, Telescope, statusline, formatter, linter.
-- ~/.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, and extensions.json under $XDG_CONFIG_HOME/Code/User/.
// 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.

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: mise or asdf; 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

    • fzf bindings, zoxide (z foo to jump), rg defaults 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).
  • 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.conf and ~/.config/zellij/config.kdl
    • ~/.config/starship.toml
  • Zsh gotcha

    • If you relocate zsh configs to XDG, set ZDOTDIR="$XDG_CONFIG_HOME/zsh" in ~/.zshenv and keep it in $HOME.

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 ~/.gitmessage template; set commit.template to point to it.

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/L to focus, Alt + Shift + H/J/K/L to move.
    • Define workspaces per role (1: code, 2: web, 3: docs/notes, 4: comms).

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 shellcheck on scripts, stylua on Lua configs, actionlint on workflows.
    • Optionally run the bootstrap in a GitHub Action with a Linux runner.

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 ~/.ssh and secrets; back up via your password manager or secure vault.

Quickstart checklist (macOS)

  • Install Xcode CLI tools
  • Install Homebrew
  • brew bundle with your Brewfile
  • Install chezmoi or set up stow+make
  • Apply dotfiles (creates symlinks into $HOME and $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