xdg-base-directory
When an application needs to store config, data, cache, or state files. When designing where user-specific files should live. When code writes to ~/.appname or hardcoded home paths. When implementing cross-platform file storage with platformdirs.
$ インストール
git clone https://github.com/Jamie-BitFlight/claude_skills /tmp/claude_skills && cp -r /tmp/claude_skills/xdg-base-directory ~/.claude/skills/claude_skills// tip: Run this command in your terminal to install the skill
name: xdg-base-directory description: When an application needs to store config, data, cache, or state files. When designing where user-specific files should live. When code writes to ~/.appname or hardcoded home paths. When implementing cross-platform file storage with platformdirs.
XDG Base Directory Specification
Implement standardized directory paths for configuration, data, cache, and state files following the XDG Base Directory Specification.
When to Use This Skill
Use this skill when:
- Creating CLI tools or applications that store user-specific files
- Implementing configuration file management for Linux/Unix applications
- Building cross-platform Python applications requiring standardized directory paths
- Migrating legacy applications from
~/.appnameto XDG-compliant paths - Designing file storage architecture for Python packages
- Implementing proper environment variable handling for user directories
- Writing applications that respect user-configured directory preferences
- Testing applications with custom XDG directory overrides
Core Specification Rules
The XDG Base Directory Specification (version 0.8, May 2021) defines these critical requirements:
1. All Paths Must Be Absolute
Environment variable values MUST be absolute paths. Relative paths are invalid and MUST be ignored.
def validate_xdg_path(path_str: str | None) -> Path | None:
"""Validate XDG path is absolute per specification."""
if not path_str:
return None
path = Path(path_str)
if not path.is_absolute():
return None # Ignore relative paths per spec
return path
2. Empty Variables Use Defaults
When an environment variable is unset or empty, use the specification-defined default value.
3. Priority Order for Search Paths
- User-specific directories (
$XDG_CONFIG_HOME,$XDG_DATA_HOME) take precedence - System directories (
$XDG_CONFIG_DIRS,$XDG_DATA_DIRS) searched in order - First match wins
4. Colon Separator for Search Paths
$XDG_DATA_DIRS and $XDG_CONFIG_DIRS use colon (:) as path separator, similar to $PATH.
Environment Variables
User-Specific Directories (Single Path)
| Variable | Purpose | Default | Use For |
|---|---|---|---|
$XDG_CONFIG_HOME | User configuration files | $HOME/.config | Settings, preferences, user configs |
$XDG_DATA_HOME | User data files | $HOME/.local/share | Databases, generated content, persistent data |
$XDG_STATE_HOME | User state files | $HOME/.local/state | Logs, history, undo buffers, recent files |
$XDG_CACHE_HOME | User cache files | $HOME/.cache | Temporary data, downloaded files, build artifacts |
$XDG_RUNTIME_DIR | User runtime files | System-set (/run/user/$UID) | Sockets, pipes, lock files, IPC |
System-Wide Directories (Search Paths)
| Variable | Purpose | Default |
|---|---|---|
$XDG_DATA_DIRS | System data search path | /usr/local/share/:/usr/share/ |
$XDG_CONFIG_DIRS | System config search path | /etc/xdg |
User Executables (Convention, Not XDG)
| Path | Purpose |
|---|---|
$HOME/.local/bin | User-specific executable files |
Note: Distributions should ensure $HOME/.local/bin appears in $PATH.
Directory Selection Guide
Choose the appropriate directory based on data characteristics:
Use $XDG_CONFIG_HOME for:
- Application settings and preferences
- User-specific configuration files
- TOML/YAML/JSON configuration
- Example:
~/.config/myapp/config.toml
Use $XDG_DATA_HOME for:
- Persistent application data
- User-generated content
- Downloaded models or assets
- Database files
- Example:
~/.local/share/myapp/models/model.gguf
Use $XDG_STATE_HOME for:
- Action history (command history, undo buffers)
- Application state that can be regenerated
- Recently used files lists
- Log files specific to user actions
- Example:
~/.local/state/myapp/history.log
Use $XDG_CACHE_HOME for:
- Temporary files safe to delete
- Downloaded data that can be re-fetched
- Build artifacts and compiled files
- Thumbnail caches
- Example:
~/.cache/myapp/downloads/
Use $XDG_RUNTIME_DIR for:
- Unix domain sockets
- Named pipes (FIFOs)
- Lock files
- Temporary IPC files
- Warning: Often tmpfs-mounted with size limits; avoid large files
- Warning: Cleaned on logout; do not store persistent data
- Example:
/run/user/1000/myapp/socket
Python Implementation Patterns
Stdlib-Only Implementation
Use for applications targeting only Linux/Unix systems or requiring no dependencies.
"""XDG Base Directory compliant path management using stdlib only."""
from pathlib import Path
import os
def get_config_home() -> Path:
"""Get XDG_CONFIG_HOME path.
Returns user-specific configuration directory.
Falls back to $HOME/.config if XDG_CONFIG_HOME unset or relative.
"""
xdg = os.environ.get('XDG_CONFIG_HOME')
if xdg and Path(xdg).is_absolute():
return Path(xdg)
return Path.home() / '.config'
def get_data_home() -> Path:
"""Get XDG_DATA_HOME path.
Returns user-specific data directory.
Falls back to $HOME/.local/share if XDG_DATA_HOME unset or relative.
"""
xdg = os.environ.get('XDG_DATA_HOME')
if xdg and Path(xdg).is_absolute():
return Path(xdg)
return Path.home() / '.local' / 'share'
def get_state_home() -> Path:
"""Get XDG_STATE_HOME path.
Returns user-specific state directory.
Falls back to $HOME/.local/state if XDG_STATE_HOME unset or relative.
"""
xdg = os.environ.get('XDG_STATE_HOME')
if xdg and Path(xdg).is_absolute():
return Path(xdg)
return Path.home() / '.local' / 'state'
def get_cache_home() -> Path:
"""Get XDG_CACHE_HOME path.
Returns user-specific cache directory.
Falls back to $HOME/.cache if XDG_CACHE_HOME unset or relative.
"""
xdg = os.environ.get('XDG_CACHE_HOME')
if xdg and Path(xdg).is_absolute():
return Path(xdg)
return Path.home() / '.cache'
def get_runtime_dir() -> Path | None:
"""Get XDG_RUNTIME_DIR path.
Returns user-specific runtime directory, or None if not set.
This variable has no default - it must be set by the system.
"""
xdg = os.environ.get('XDG_RUNTIME_DIR')
if xdg and Path(xdg).is_absolute():
return Path(xdg)
return None
def get_config_dirs() -> list[Path]:
"""Get XDG_CONFIG_DIRS as list of paths.
Returns preference-ordered list of system configuration directories.
Falls back to [Path('/etc/xdg')] if XDG_CONFIG_DIRS unset.
"""
xdg = os.environ.get('XDG_CONFIG_DIRS')
if xdg:
paths = [Path(p) for p in xdg.split(':') if p and Path(p).is_absolute()]
if paths:
return paths
return [Path('/etc/xdg')]
def get_data_dirs() -> list[Path]:
"""Get XDG_DATA_DIRS as list of paths.
Returns preference-ordered list of system data directories.
Falls back to [Path('/usr/local/share'), Path('/usr/share')] if unset.
"""
xdg = os.environ.get('XDG_DATA_DIRS')
if xdg:
paths = [Path(p) for p in xdg.split(':') if p and Path(p).is_absolute()]
if paths:
return paths
return [Path('/usr/local/share'), Path('/usr/share')]
Application-Specific Path Module
Create a dedicated paths module for your application:
"""Path management for myapp following XDG specification."""
from pathlib import Path
import os
APP_NAME = 'myapp'
def get_config_dir() -> Path:
"""Get myapp config directory."""
xdg = os.environ.get('XDG_CONFIG_HOME')
base = Path(xdg) if xdg and Path(xdg).is_absolute() else Path.home() / '.config'
return base / APP_NAME
def get_config_file() -> Path:
"""Get myapp config file path."""
return get_config_dir() / 'config.toml'
def get_data_dir() -> Path:
"""Get myapp data directory."""
xdg = os.environ.get('XDG_DATA_HOME')
base = Path(xdg) if xdg and Path(xdg).is_absolute() else Path.home() / '.local' / 'share'
return base / APP_NAME
def get_cache_dir() -> Path:
"""Get myapp cache directory."""
xdg = os.environ.get('XDG_CACHE_HOME')
base = Path(xdg) if xdg and Path(xdg).is_absolute() else Path.home() / '.cache'
return base / APP_NAME
def get_state_dir() -> Path:
"""Get myapp state directory."""
xdg = os.environ.get('XDG_STATE_HOME')
base = Path(xdg) if xdg and Path(xdg).is_absolute() else Path.home() / '.local' / 'state'
return base / APP_NAME
def ensure_directories() -> None:
"""Create all required directories with appropriate permissions."""
for directory in [get_config_dir(), get_data_dir(), get_cache_dir(), get_state_dir()]:
directory.mkdir(parents=True, exist_ok=True)
Cross-Platform Implementation with platformdirs
For applications targeting Linux, macOS, and Windows, use the platformdirs library:
"""Cross-platform path management using platformdirs."""
from platformdirs import user_config_dir, user_data_dir, user_cache_dir, user_state_dir
from pathlib import Path
APP_NAME = 'myapp'
APP_AUTHOR = 'myapp' # Used on Windows
# Get platform-appropriate directories
# Linux: Uses XDG specification
# macOS: Uses ~/Library/Application Support, ~/Library/Caches
# Windows: Uses %APPDATA%, %LOCALAPPDATA%
config_dir = Path(user_config_dir(APP_NAME, APP_AUTHOR))
data_dir = Path(user_data_dir(APP_NAME, APP_AUTHOR))
cache_dir = Path(user_cache_dir(APP_NAME, APP_AUTHOR))
state_dir = Path(user_state_dir(APP_NAME, APP_AUTHOR))
# Auto-create directories
config_dir = Path(user_config_dir(APP_NAME, APP_AUTHOR, ensure_exists=True))
Platform-specific paths:
| Platform | Config | Data | Cache | State |
|---|---|---|---|---|
| Linux | ~/.config/myapp | ~/.local/share/myapp | ~/.cache/myapp | ~/.local/state/myapp |
| macOS | ~/Library/Application Support/myapp | ~/Library/Application Support/myapp | ~/Library/Caches/myapp | ~/Library/Application Support/myapp |
| Windows | C:\Users\<user>\AppData\Local\myapp\myapp | C:\Users\<user>\AppData\Local\myapp\myapp | C:\Users\<user>\AppData\Local\myapp\myapp\Cache | C:\Users\<user>\AppData\Local\myapp\myapp |
When to use platformdirs:
- Application targets multiple operating systems
- Need native platform conventions (macOS
~/Library, Windows%APPDATA%) - Want automatic directory creation with
ensure_exists=True
When to use stdlib-only:
- Application targets only Linux/Unix
- Minimize dependencies
- Need complete control over path resolution logic
Common Anti-Patterns
1. Using ~/.appname (Legacy Pattern)
Problem: Violates XDG specification, clutters home directory.
# ❌ WRONG
config_file = Path.home() / '.myapp' / 'config.toml'
Solution: Use ~/.config/appname/
# ✅ CORRECT
def get_config_dir() -> Path:
xdg = os.environ.get('XDG_CONFIG_HOME')
base = Path(xdg) if xdg and Path(xdg).is_absolute() else Path.home() / '.config'
return base / 'myapp'
config_file = get_config_dir() / 'config.toml'
2. Ignoring Environment Variables
Problem: Hardcoded paths prevent user customization.
# ❌ WRONG
config_dir = Path.home() / '.config' / 'myapp'
Solution: Always check environment variables first.
# ✅ CORRECT
xdg = os.environ.get('XDG_CONFIG_HOME')
base = Path(xdg) if xdg and Path(xdg).is_absolute() else Path.home() / '.config'
config_dir = base / 'myapp'
3. Accepting Relative Paths
Problem: XDG specification mandates absolute paths only.
# ❌ WRONG
xdg = os.environ.get('XDG_CONFIG_HOME', str(Path.home() / '.config'))
return Path(xdg) # Accepts relative paths
Solution: Validate absolute paths, fall back to default for relative.
# ✅ CORRECT
xdg = os.environ.get('XDG_CONFIG_HOME')
if xdg and Path(xdg).is_absolute():
return Path(xdg)
return Path.home() / '.config' # Default for unset or relative
4. Missing Directory Creation
Problem: Writing files without ensuring parent directories exist.
# ❌ WRONG
config_file = get_config_dir() / 'config.toml'
config_file.write_text(data) # Fails if directory doesn't exist
Solution: Create directories before writing files.
# ✅ CORRECT
config_file = get_config_dir() / 'config.toml'
config_file.parent.mkdir(parents=True, exist_ok=True)
config_file.write_text(data)
5. Storing Cache in Config Directory
Problem: Mixing regenerable data with configuration.
# ❌ WRONG
cache_file = get_config_dir() / 'download_cache.json'
Solution: Use $XDG_CACHE_HOME for regenerable data.
# ✅ CORRECT
cache_file = get_cache_dir() / 'download_cache.json'
6. Large Files in Runtime Directory
Problem: $XDG_RUNTIME_DIR often tmpfs-mounted with size limits.
# ❌ WRONG
large_file = get_runtime_dir() / 'model.gguf' # May be 1GB+
Solution: Use $XDG_DATA_HOME for large persistent files.
# ✅ CORRECT
large_file = get_data_dir() / 'models' / 'model.gguf'
7. Not Handling Unset XDG_RUNTIME_DIR
Problem: $XDG_RUNTIME_DIR has no default value.
# ❌ WRONG
runtime_dir = get_runtime_dir()
socket_path = runtime_dir / 'socket' # Fails if None
Solution: Check for None before using.
# ✅ CORRECT
runtime_dir = get_runtime_dir()
if runtime_dir is None:
raise RuntimeError("XDG_RUNTIME_DIR not set by system")
socket_path = runtime_dir / 'socket'
Testing XDG Compliance
Manual Testing with Environment Variables
# Test with custom XDG directories
export XDG_CONFIG_HOME=/tmp/test-config
export XDG_DATA_HOME=/tmp/test-data
export XDG_CACHE_HOME=/tmp/test-cache
export XDG_STATE_HOME=/tmp/test-state
# Run application
myapp --help
# Verify files are in correct locations
ls -la /tmp/test-config/myapp/
ls -la /tmp/test-data/myapp/
ls -la /tmp/test-cache/myapp/
ls -la /tmp/test-state/myapp/
# Test with unset variables (should use defaults)
unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME
myapp --help
ls -la ~/.config/myapp/
ls -la ~/.local/share/myapp/
ls -la ~/.cache/myapp/
ls -la ~/.local/state/myapp/
Automated Testing
"""Test XDG compliance."""
import os
import tempfile
from pathlib import Path
import pytest
def test_xdg_config_home_override(monkeypatch):
"""XDG_CONFIG_HOME environment variable is respected."""
with tempfile.TemporaryDirectory() as tmpdir:
monkeypatch.setenv('XDG_CONFIG_HOME', tmpdir)
config_dir = get_config_dir()
assert config_dir == Path(tmpdir) / 'myapp'
def test_xdg_config_home_default(monkeypatch):
"""Default used when XDG_CONFIG_HOME unset."""
monkeypatch.delenv('XDG_CONFIG_HOME', raising=False)
config_dir = get_config_dir()
assert config_dir == Path.home() / '.config' / 'myapp'
def test_relative_path_ignored(monkeypatch):
"""Relative paths in XDG variables are ignored per specification."""
monkeypatch.setenv('XDG_CONFIG_HOME', 'relative/path')
config_dir = get_config_dir()
# Should fall back to default, not use relative path
assert config_dir == Path.home() / '.config' / 'myapp'
def test_empty_string_uses_default(monkeypatch):
"""Empty string in XDG variable uses default."""
monkeypatch.setenv('XDG_CONFIG_HOME', '')
config_dir = get_config_dir()
assert config_dir == Path.home() / '.config' / 'myapp'
def test_runtime_dir_none_when_unset(monkeypatch):
"""XDG_RUNTIME_DIR returns None when unset (no default)."""
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
runtime_dir = get_runtime_dir()
assert runtime_dir is None
def test_config_dirs_search_path(monkeypatch):
"""XDG_CONFIG_DIRS parsed as colon-separated search path."""
monkeypatch.setenv('XDG_CONFIG_DIRS', '/etc/xdg:/opt/config')
dirs = get_config_dirs()
assert dirs == [Path('/etc/xdg'), Path('/opt/config')]
def test_data_dirs_ignores_relative_paths(monkeypatch):
"""XDG_DATA_DIRS filters out relative paths."""
monkeypatch.setenv('XDG_DATA_DIRS', '/usr/share:relative/path:/opt/data')
dirs = get_data_dirs()
assert dirs == [Path('/usr/share'), Path('/opt/data')]
Example Directory Structure
~/.config/myapp/ # XDG_CONFIG_HOME
config.toml # Main configuration
credentials.json # User credentials
~/.local/share/myapp/ # XDG_DATA_HOME
models/ # Downloaded models
model-v1.gguf
databases/
user.db
~/.local/state/myapp/ # XDG_STATE_HOME
history.log # Command history
recent-files.json # Recently used files
~/.local/bin/ # User binaries (convention)
myapp # Application binary
~/.cache/myapp/ # XDG_CACHE_HOME
downloads/ # Downloaded temporary files
build/ # Build artifacts
Configuration File Loading Pattern
For TOML configuration files with XDG support, activate the toml-python skill:
Skill(command: "toml-python")
The toml-python skill provides comprehensive guidance on TOML parsing with tomllib (Python 3.11+) and tomli (backport), including validation with Pydantic models.
Related Skills
Activate these skills for related functionality:
toml-python- TOML configuration file parsing and validationpython3-development- Modern Python development patterns and best practicesuv- Python package and project management
References
Official Specification
- XDG Base Directory Specification v0.8 - Primary authoritative source (accessed 2025-01-15)
- Freedesktop.org Specifications Index - All freedesktop specifications
- XDG Specs Git Repository - Source repository
Implementation Guides
- ArchWiki: XDG Base Directory - Comprehensive implementation guide (accessed 2025-01-15)
- platformdirs Python Library - Cross-platform Python implementation (accessed 2025-01-15)
Community Resources
- XDG Mailing List - Official discussion forum
- Freedesktop.org Wiki - Additional documentation
Key Principles
- Absolute Paths Only: Validate all XDG paths are absolute; ignore relative paths
- Environment Variable Priority: Always check XDG environment variables before defaults
- Specification Compliance: Follow XDG Base Directory Specification v0.8 exactly
- Directory Creation: Create parent directories before writing files
- Appropriate Storage: Use correct directory for data type (config vs data vs cache vs state)
- Runtime Directory Limits: Avoid large files in
$XDG_RUNTIME_DIR(tmpfs limits) - Cross-Platform Awareness: Use platformdirs for macOS/Windows support
- Search Path Handling: Parse colon-separated search paths correctly
- No Default for Runtime:
$XDG_RUNTIME_DIRhas no default; check for None - Testing: Validate XDG compliance with environment variable overrides
Repository
