Skip to content

XDG Base Directory Specification

Most configs in this repo live under ~/.config/, ~/.local/share/, ~/.local/state/, or ~/.cache/ rather than dot-files in $HOME. That layout follows the XDG Base Directory Specification.

Core environment variables

Variable Default Purpose
XDG_CONFIG_HOME ~/.config Per-user config files
XDG_DATA_HOME ~/.local/share Per-user application data
XDG_STATE_HOME ~/.local/state Per-user state (logs, history, undo)
XDG_CACHE_HOME ~/.cache Per-user non-essential cached data
XDG_RUNTIME_DIR (set by login) Per-session runtime files (sockets, etc.)

Why it matters

  • Clean $HOME: the old "every app dumps a ~/.foo dotfile" pattern gets noisy fast.
  • Predictable tooling: backup, sync, dotfile managers (like chezmoi) all benefit from one well-known tree.
  • Easy to nuke: wipe ~/.cache/foo without touching configs; wipe ~/.local/state/foo without losing settings.

Choosing the right directory

When writing a new tool — or deciding where to redirect an existing one's state — use this cheat-sheet. The first four rows are XDG-spec; the last two are what programs typically reach for once they leave the spec's coverage.

Data category Where it goes Default path Notes
Important config (user must back up) XDG_CONFIG_HOME ~/.config/mytool/ Editable, declarative — config.toml, keymap.json. Survives uninstall/reinstall; safe to commit to a dotfiles repo.
Important data (user must back up) XDG_DATA_HOME ~/.local/share/mytool/ Generated by the app but irreplaceable: notes DB, plugin store, downloaded models, generated assets.
Persistent state (machine-local, not portable) XDG_STATE_HOME ~/.local/state/mytool/ History, undo trees, last-opened files, cursor position, log files. Annoying to lose but auto-rebuilds.
Reconstructible cache XDG_CACHE_HOME ~/.cache/mytool/ Compiled artifacts, downloaded blobs, thumbnail tiles. Wipe anytime.
Per-session runtime files XDG_RUNTIME_DIR /run/user/$UID/mytool/ Unix sockets, lock files, named pipes. Mode 0700, tmpfs, deleted on logout. Linux-only — macOS does not set this; fall back to $TMPDIR.
One-shot ephemeral scratch $TMPDIR (or /tmp) /tmp (Linux), /var/folders/.../T/ (macOS) Process-scoped intermediate files. Not XDG — POSIX/FHS territory. Deleted on reboot or by systemd-tmpfiles / tmpwatch. Always honor $TMPDIR when set; mktemp -d -p "${TMPDIR:-/tmp}".

Decision rules, in order:

  1. "Will the user be sad if it disappears?" — yes → CONFIG_HOME or DATA_HOME. No → one of the lower tiers.
  2. "Did the user hand-write it?" — yes → CONFIG_HOME. No, but app-generated and irreplaceable → DATA_HOME.
  3. "Could I regenerate it from source/network?" — yes → CACHE_HOME. No, and it's nice-to-keep across runs → STATE_HOME.
  4. "Does it need a Unix socket / lock other processes can find?"RUNTIME_DIR (Linux) / $TMPDIR (macOS). Use a stable name ($XDG_RUNTIME_DIR/mytool.sock) so peers can locate it.
  5. "Scoped to a single process invocation?"mktemp -d -p "${TMPDIR:-/tmp}" mytool.XXXXXX and trap 'rm -rf "$tmp"' EXIT.

Common gotchas:

  • XDG_RUNTIME_DIR is not set on macOS by default (no systemd-logind). Hardcoding it breaks; the cross-platform idiom is ${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}.
  • /tmp on Linux may or may not be tmpfs (distro / systemd-tmpfiles dependent). Do not assume it survives a reboot, and do not assume it persists across one either.
  • The XDG spec says nothing about /tmp — that's POSIX/FHS. Don't conflate the two.
  • XDG_STATE_HOME was only added in spec v0.8 (2021). Older tools predate it and often dump state into XDG_DATA_HOME or ~/.local/share/; that's a historical wart, not a bug to "fix" upstream.
  • macOS's ~/Library/Application Support/ is the platform-native equivalent of XDG_DATA_HOME; many cross-platform apps detect Darwin and use it instead. That's expected — it's not a regression.

Tool-by-tool status (in this repo)

Tool XDG-native? Where we put it
tmux Yes (3.1+) ~/.config/tmux/tmux.conf (+ shim at ~/.tmux.conf)
Neovim Yes ~/.config/nvim/
Zellij Yes ~/.config/zellij/
Starship Yes ~/.config/starship.toml
Alacritty Yes ~/.config/alacritty/
Ghostty Yes ~/.config/ghostty/
Yazi Yes ~/.config/yazi/
bat Yes ~/.config/bat/
direnv Yes ~/.config/direnv/
gh Yes ~/.config/gh/
gh-dash Yes ~/.config/gh-dash/
LazyGit Yes ~/.config/lazygit/
Sesh Yes ~/.config/sesh/
uv Yes ~/.config/uv/
Bun Yes ~/.config/.bunfig.toml
Homebrew Bundle N/A ~/.config/homebrew/ (our convention)
Claude Code No ~/.claude/
SSH No ~/.ssh/ (spec doesn't cover it)
Git Partial ~/.gitconfig (repo also uses ~/.config/git/hooks/)
npm Yes ~/.npmrc (via $HOME; XDG support exists but most tooling still writes ~/.npmrc)
Cargo Partial ~/.cargo/config.toml (respects CARGO_HOME, not XDG directly)

Tools marked "No" or "Partial" keep legacy $HOME paths because upstream hasn't migrated or because the tool predates the spec; we follow whatever the tool actually reads.

How this repo handles it

  • chezmoi source paths like dot_config/<tool>/... map to ~/.config/<tool>/... on apply. That is the primary way we opt into XDG.
  • Legacy paths (e.g. ~/.tmux.conf, ~/.gitconfig) are kept as shims or top-level files when a tool expects them, often sourcing the real XDG-located config.
  • For state/data/cache: we rarely commit those — they're generated at runtime and deliberately excluded from the repo.

Further reading