XDG Base Directory Specification

Quick Reference

# XDG directories
echo $XDG_CONFIG_HOME    # ~/.config
echo $XDG_DATA_HOME      # ~/.local/share
echo $XDG_CACHE_HOME     # ~/.cache
echo $XDG_STATE_HOME     # ~/.local/state
echo $XDG_RUNTIME_DIR    # /run/user/UID

# Check if set, use default if not
${XDG_CONFIG_HOME:-$HOME/.config}
${XDG_DATA_HOME:-$HOME/.local/share}

# List XDG user directories
cat ~/.config/user-dirs.dirs
xdg-user-dir DESKTOP
xdg-user-dir DOWNLOAD

Understanding XDG Standards

Why XDG?

The XDG Base Directory Specification solves the "dotfile mess" in home directories:

Without XDG:

~/.bashrc
~/.bash_history
~/.vimrc
~/.vim/
~/.gitconfig
~/.npmrc
~/.cargo/
~/.local/bin/
... hundreds more

With XDG:

~/.config/          # Configuration files
~/.local/share/     # Application data
~/.cache/           # Cached data
~/.local/state/     # State data

Benefits

  • Organized home directory - Clear separation of concerns

  • Backup simplicity - Back up ~/.config for settings

  • Easy cleanup - Delete ~/.cache safely

  • Multi-profile - Different configs per environment

  • Portable configs - Share dotfiles easily

XDG Environment Variables

Required Variables

Variable Default Purpose

XDG_CONFIG_HOME

~/.config

User-specific configuration

XDG_DATA_HOME

~/.local/share

User-specific data files

XDG_CACHE_HOME

~/.cache

User-specific cache data

XDG_STATE_HOME

~/.local/state

User-specific state data

XDG_RUNTIME_DIR

/run/user/UID

Runtime files (sockets, etc.)

System-Wide Variables

Variable Default Purpose

XDG_DATA_DIRS

/usr/local/share:/usr/share

System data search paths

XDG_CONFIG_DIRS

/etc/xdg

System config search paths

Set XDG Variables

# ~/.bashrc or ~/.zshrc
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"

# Create directories if needed
mkdir -p "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_CACHE_HOME" "$XDG_STATE_HOME"

Using Variables in Scripts

# Always use fallback for portability
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/myapp"
data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/myapp"
cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/myapp"
state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/myapp"

# Create app directories
mkdir -p "$config_dir" "$data_dir" "$cache_dir" "$state_dir"

Directory Structure

XDG_CONFIG_HOME (~/.config)

Configuration files that users may edit:

~/.config/
├── git/
│   └── config              # Git configuration
├── nvim/
│   └── init.lua            # Neovim configuration
├── systemd/
│   └── user/               # User systemd units
├── containers/
│   └── containers.conf     # Podman configuration
├── alacritty/
│   └── alacritty.toml      # Terminal configuration
└── hypr/
    └── hyprland.conf       # Hyprland configuration

XDG_DATA_HOME (~/.local/share)

Application data that should be preserved:

~/.local/share/
├── applications/           # Desktop entries
├── fonts/                  # User fonts
├── icons/                  # User icons
├── nvim/
│   └── site/               # Neovim plugins
├── gnupg/                  # GPG keyrings
├── password-store/         # pass passwords
└── containers/
    └── storage/            # Podman images

XDG_CACHE_HOME (~/.cache)

Non-essential cached data (can be deleted):

~/.cache/
├── pip/                    # Python packages
├── npm/                    # Node packages
├── go-build/               # Go build cache
├── thumbnails/             # Image thumbnails
└── fontconfig/             # Font cache

XDG_STATE_HOME (~/.local/state)

State data that should persist but isn’t config:

~/.local/state/
├── bash/
│   └── history             # Bash history
├── lesshst                 # Less history
├── nvim/
│   └── shada/              # Neovim state
└── wireplumber/            # Audio state

XDG_RUNTIME_DIR (/run/user/UID)

Runtime files (sockets, temporary):

/run/user/1000/
├── pulse/                  # PulseAudio socket
├── bus                     # D-Bus session socket
├── wayland-0               # Wayland socket
├── gnupg/                  # GPG agent socket
└── podman/                 # Podman socket

XDG User Directories

Standard User Directories

# View current settings
cat ~/.config/user-dirs.dirs

# Standard directories
XDG_DESKTOP_DIR="$HOME/Desktop"
XDG_DOWNLOAD_DIR="$HOME/Downloads"
XDG_TEMPLATES_DIR="$HOME/Templates"
XDG_PUBLICSHARE_DIR="$HOME/Public"
XDG_DOCUMENTS_DIR="$HOME/Documents"
XDG_MUSIC_DIR="$HOME/Music"
XDG_PICTURES_DIR="$HOME/Pictures"
XDG_VIDEOS_DIR="$HOME/Videos"

Query User Directories

# Get specific directory
xdg-user-dir DESKTOP
xdg-user-dir DOWNLOAD
xdg-user-dir DOCUMENTS

# In scripts
downloads=$(xdg-user-dir DOWNLOAD)

Customize User Directories

# Edit ~/.config/user-dirs.dirs
XDG_DESKTOP_DIR="$HOME/desktop"
XDG_DOWNLOAD_DIR="$HOME/downloads"
XDG_DOCUMENTS_DIR="$HOME/documents"

# Update directories
xdg-user-dirs-update

# Prevent automatic updates
echo "enabled=False" > ~/.config/user-dirs.conf

Application Compliance

Applications with XDG Support

Many applications support XDG natively:

# Git (since 2.x)
~/.config/git/config
~/.config/git/ignore

# Neovim
~/.config/nvim/init.lua

# Tmux
~/.config/tmux/tmux.conf

# Fish shell
~/.config/fish/config.fish

# Alacritty
~/.config/alacritty/alacritty.toml

# mpv
~/.config/mpv/mpv.conf

# systemd user units
~/.config/systemd/user/

Force XDG Compliance

Some applications need environment variables:

# ~/.bashrc or ~/.zshrc

# Bash
export HISTFILE="${XDG_STATE_HOME:-$HOME/.local/state}/bash/history"
mkdir -p "$(dirname "$HISTFILE")"

# Less
export LESSHISTFILE="${XDG_STATE_HOME:-$HOME/.local/state}/lesshst"

# Wget
export WGETRC="${XDG_CONFIG_HOME:-$HOME/.config}/wget/wgetrc"
alias wget='wget --hsts-file="${XDG_DATA_HOME:-$HOME/.local/share}/wget/hsts"'

# GNU Readline
export INPUTRC="${XDG_CONFIG_HOME:-$HOME/.config}/readline/inputrc"

# Python
export PYTHONSTARTUP="${XDG_CONFIG_HOME:-$HOME/.config}/python/startup.py"
export PYTHON_HISTORY="${XDG_STATE_HOME:-$HOME/.local/state}/python/history"

# Node.js
export NODE_REPL_HISTORY="${XDG_STATE_HOME:-$HOME/.local/state}/node/history"
export NPM_CONFIG_USERCONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/npm/npmrc"
export NPM_CONFIG_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/npm"

# Rust/Cargo
export CARGO_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/cargo"
export RUSTUP_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/rustup"

# Go
export GOPATH="${XDG_DATA_HOME:-$HOME/.local/share}/go"
export GOMODCACHE="${XDG_CACHE_HOME:-$HOME/.cache}/go/mod"

# Docker
export DOCKER_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/docker"

# GnuPG
export GNUPGHOME="${XDG_DATA_HOME:-$HOME/.local/share}/gnupg"

# Pass (password-store)
export PASSWORD_STORE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/pass"

Applications That Ignore XDG

Some applications still use dotfiles:

# These typically can't be moved:
~/.ssh/                 # SSH keys and config (security reasons)
~/.bashrc               # Sourced by bash before profile
~/.profile              # Login shell config
~/.Xauthority           # X11 auth (varies)

XDG MIME Applications

Default Applications

# View default application for type
xdg-mime query default text/html
xdg-mime query default application/pdf

# Set default application
xdg-mime default firefox.desktop text/html
xdg-mime default org.pwmt.zathura.desktop application/pdf

# Query file type
xdg-mime query filetype document.pdf

MIME Database

# User MIME associations
~/.config/mimeapps.list

# Example mimeapps.list
[Default Applications]
text/html=firefox.desktop
application/pdf=org.pwmt.zathura.desktop
image/png=imv.desktop
video/mp4=mpv.desktop

[Added Associations]
text/plain=nvim.desktop;code.desktop

# Update MIME database
update-mime-database ~/.local/share/mime

xdg-open

# Open file with default application
xdg-open document.pdf
xdg-open https://example.com
xdg-open image.png

# xdg-open uses mimeapps.list to determine application

Desktop Entries

Desktop Entry Location

# User desktop entries
~/.local/share/applications/

# System desktop entries
/usr/share/applications/

# Desktop entry example
~/.local/share/applications/myapp.desktop

Create Desktop Entry

# ~/.local/share/applications/myapp.desktop
cat > ~/.local/share/applications/myapp.desktop << 'EOF'
[Desktop Entry]
Type=Application
Name=My Application
Comment=A cool application
Exec=/usr/bin/myapp %f
Icon=myapp
Terminal=false
Categories=Utility;
MimeType=text/plain;
EOF

# Update desktop database
update-desktop-database ~/.local/share/applications

Desktop Entry Fields

Field Description

Type

Application, Link, or Directory

Name

Display name

Exec

Command to execute

Icon

Icon name or path

Terminal

true if needs terminal

Categories

Application categories

MimeType

Handled MIME types

NoDisplay

Hide from menus

Hidden

Completely hidden

Autostart

XDG Autostart

# User autostart directory
~/.config/autostart/

# System autostart
/etc/xdg/autostart/

# Create autostart entry
cat > ~/.config/autostart/myapp.desktop << 'EOF'
[Desktop Entry]
Type=Application
Name=My Startup App
Exec=/usr/bin/myapp --daemon
Hidden=false
NoDisplay=false
X-GNOME-Autostart-enabled=true
EOF

Conditional Autostart

# Only on specific desktop
[Desktop Entry]
Type=Application
Name=GNOME App
Exec=/usr/bin/gnome-app
OnlyShowIn=GNOME;

# Exclude from specific desktop
[Desktop Entry]
Type=Application
Name=Not KDE App
Exec=/usr/bin/app
NotShowIn=KDE;

Managing XDG Files

Backup Strategy

# Back up configuration
tar czf config-backup.tar.gz -C ~ .config

# Back up important data
tar czf data-backup.tar.gz -C ~/.local share

# Exclude cache from backup
# Cache can be regenerated

Clean Up Cache

# Safe to delete entirely
rm -rf ~/.cache/*

# Or selectively
rm -rf ~/.cache/pip
rm -rf ~/.cache/npm
rm -rf ~/.cache/thumbnails

# Check cache size
du -sh ~/.cache/*/

Sync Configs

# Use symlinks to manage dotfiles
# ~/.config/git -> ~/dotfiles/git
# ~/.config/nvim -> ~/dotfiles/nvim

# Or use stow
cd ~/dotfiles
stow git    # Links git/* to ~/.config/git/*
stow nvim   # Links nvim/* to ~/.config/nvim/*

Troubleshooting

Check XDG Variables

# Print all XDG variables
env | grep XDG

# Verify directories exist
ls -la "${XDG_CONFIG_HOME:-$HOME/.config}"
ls -la "${XDG_DATA_HOME:-$HOME/.local/share}"
ls -la "${XDG_CACHE_HOME:-$HOME/.cache}"
ls -la "${XDG_STATE_HOME:-$HOME/.local/state}"
ls -la "${XDG_RUNTIME_DIR}"

Find Non-XDG Dotfiles

# List dotfiles in home (potential XDG violations)
ls -la ~ | grep '^\.'

# Common offenders
~/.bash_history      # Should be XDG_STATE_HOME
~/.python_history    # Should be XDG_STATE_HOME
~/.wget-hsts         # Should be XDG_DATA_HOME
~/.lesshst           # Should be XDG_STATE_HOME

Runtime Directory Issues

# XDG_RUNTIME_DIR not set (common in cron/systemd)
if [ -z "$XDG_RUNTIME_DIR" ]; then
    export XDG_RUNTIME_DIR="/run/user/$(id -u)"
fi

# Verify permissions
ls -la /run/user/$(id -u)
# Should be owned by user, mode 0700

Quick Reference

# Environment variables
XDG_CONFIG_HOME    ~/.config           # User configuration
XDG_DATA_HOME      ~/.local/share      # User data
XDG_CACHE_HOME     ~/.cache            # User cache
XDG_STATE_HOME     ~/.local/state      # User state
XDG_RUNTIME_DIR    /run/user/UID       # Runtime files

# User directories
xdg-user-dir DESKTOP                   # ~/Desktop
xdg-user-dir DOWNLOAD                  # ~/Downloads
xdg-user-dir DOCUMENTS                 # ~/Documents

# MIME handling
xdg-mime query default TYPE            # Get default app
xdg-mime default APP.desktop TYPE      # Set default app
xdg-open FILE                          # Open with default

# Locations
~/.config/mimeapps.list                # MIME associations
~/.local/share/applications/           # Desktop entries
~/.config/autostart/                   # Autostart entries
~/.config/user-dirs.dirs               # User directory config