Back to articles

Understanding Config File Organization

Computer TechSystem ConfigurationUnderstanding Config File Organization
Updated 4/23/2026

Understanding Config File Organization

XDG Base Directory Specification

The XDG Base Directory Specification is a standard that defines where applications should store their configuration files, data, and cache on Unix-like systems (Linux, macOS).

Key directories:

VariableDefault LocationPurposeExample
XDG_CONFIG_HOME~/.config/User-specific configurationsShell configs, editor settings
XDG_DATA_HOME~/.local/share/User-specific data filesApplication state, databases
XDG_CACHE_HOME~/.local/cache/Non-essential cached dataDownloads, thumbnails
XDG_STATE_HOME~/.local/state/Logs, history, recent filesCommand history, undo data

Why this matters:

  • Keeps home directory clean (no more 50 dotfiles in ~)
  • Makes backups easier (just backup ~/.config/ and ~/.local/)
  • Standardizes where tools look for configs
  • Works seamlessly with dotfile managers like chezmoi

From your screenshot, you have:

~/.config/
├── zellij/
├── yarn/
├── zsh/           ← We'll set this up
├── nushell/       ← Future shell
├── fish/          ← Future shell
├── nvim/
├── karabiner/
├── htop/
├── gh/
├── env/
├── configstore/
└── chezmoi/

The conf.d Pattern

The conf.d/ pattern (configuration directory) is a modular approach where instead of one monolithic config file, you have a directory of smaller, focused files that get loaded automatically.

How it works:

~/.config/zsh/
├── .zshrc                    # Main loader file
└── conf.d/                   # Modular configs
    ├── 00-environment.zsh    # Env vars (PATH, EDITOR, etc.)
    ├── 10-aliases.zsh        # Command aliases
    ├── 20-functions.zsh      # Custom functions
    ├── 30-completion.zsh     # Tab completion
    ├── 40-keybindings.zsh    # Key mappings
    └── 99-local.zsh          # Machine-specific overrides

Why use numbered prefixes (00-, 10-, etc.)?

  • Controls load order (environment vars before aliases)
  • Alphabetical sorting ensures predictable behavior
  • Easy to insert new configs between existing ones
  • Convention: increment by 10 to leave room for additions

The main loader (.zshrc or init.zsh) typically looks like:

bash
# Load all configs in order
for config in ~/.config/zsh/conf.d/*.zsh; do
  [ -f "$config" ] && source "$config"
done

Shell-Specific Config Locations

Different shells have different conventions, but modern shells support XDG:

Zsh

Traditional: ~/.zshrc, ~/.zprofile, ~/.zshenv
XDG-compliant:

~/.config/zsh/
├── .zshenv       # Always sourced (set ZDOTDIR here)
├── .zprofile     # Login shells
├── .zshrc        # Interactive shells
└── conf.d/       # Modular configs

Key trick: Set ZDOTDIR in ~/.zshenv to point to ~/.config/zsh/, then zsh looks there for all other configs.

Nushell

Native XDG support:

~/.config/nushell/
├── config.nu     # Main config
├── env.nu        # Environment variables
└── scripts/      # Custom commands (nu files)

Nushell loads env.nu first, then config.nu. No need for conf.d pattern because you can use or source files explicitly:

nu
# In config.nu
source ~/.config/nushell/scripts/aliases.nu
source ~/.config/nushell/scripts/custom-commands.nu

Fish

Native XDG support:

~/.config/fish/
├── config.fish           # Main config
├── fish_variables        # Universal variables
└── conf.d/               # Auto-loaded configs
    ├── aliases.fish
    ├── environment.fish
    └── functions.fish

Fish automatically sources all files in conf.d/ in alphabetical order. No manual loading needed.

Why Modular > Monolithic

Monolithic approach (old way):

bash
# ~/.zshrc (500 lines)
export PATH="..."
export EDITOR="..."
alias ls="eza"
alias cat="bat"
function myfunc() { ... }
# ... 400 more lines

Problems:

  • Hard to find specific settings
  • Merge conflicts when syncing between machines
  • Can't easily disable one section
  • Comments become messy

Modular approach (modern way):

conf.d/
├── 00-environment.zsh    # 20 lines of env vars
├── 10-aliases.zsh        # 30 lines of aliases
├── 20-functions.zsh      # Custom functions
└── 99-local.zsh          # Machine-specific (git-ignored)

Benefits:

  • Find settings instantly (need to change alias? → 10-aliases.zsh)
  • Disable a module by renaming it (.zsh.zsh.disabled)
  • Share common configs across shells (environment.zsh → environment.fish)
  • Git history shows which module changed
  • Machine-specific configs can be git-ignored or templated

How This Works with Chezmoi

Chezmoi manages your dotfiles in ~/.local/share/chezmoi/ and syncs them to their target locations.

For ~/.config/zsh/, chezmoi would store:

~/.local/share/chezmoi/
└── dot_config/
    └── zsh/
        ├── dot_zshrc
        └── conf.d/
            ├── 00-environment.zsh
            ├── 10-aliases.zsh
            └── 99-local.zsh.tmpl    # Template for machine-specific

When you run chezmoi apply, it creates:

~/.config/zsh/
├── .zshrc
└── conf.d/
    ├── 00-environment.zsh
    ├── 10-aliases.zsh
    └── 99-local.zsh              # Generated from template

Templates let you customize per-machine:

bash
# 99-local.zsh.tmpl
{{- if eq .chezmoi.hostname "macbook-pro" }}
export WORK_MODE=true
{{- else }}
export WORK_MODE=false
{{- end }}

Based on your current shell-config/zsh/ files:

New structure:

~/.config/zsh/
├── .zshrc                        # Main loader (sources conf.d/*)
└── conf.d/
    ├── 00-environment.zsh        # Your current environment.zsh
    ├── 10-aliases.zsh            # Your current aliases.zsh
    ├── 20-display.zsh            # Your current display.zsh
    └── 99-local.zsh              # Machine-specific (optional)

Your existing .zshrc stays at ~/.zshrc (because zsh expects it there unless you set ZDOTDIR), but it will source from the new location:

bash
# ~/.zshrc
# Load modular configs
for config in ~/.config/zsh/conf.d/*.zsh; do
  [ -f "$config" ] && source "$config"
done

# Remaining zsh-specific setup (asdf, zoxide, starship)
# ...

Summary

XDG: Standard locations (~/.config/, ~/.local/share/, etc.)
conf.d: Modular files auto-loaded in alphabetical order
Numbered prefixes: Control load order, leave room for growth
Chezmoi: Manages these files, syncs across machines

Your new workflow:

  1. Edit configs in ~/.config/zsh/conf.d/10-aliases.zsh
  2. Chezmoi tracks changes automatically
  3. Push to Git, pull on other machines
  4. All shells (zsh, nushell, fish) use same XDG structure

Next steps:

  • Migrate shell-config/zsh/~/.config/zsh/conf.d/
  • Add to chezmoi management
  • Set up similar structure for nushell, fish when you switch shells