Help us improve
Share bugs, ideas, or general feedback.
From xdg-base-directory
Implements XDG Base Directory Specification for storing config, data, cache, and state files in Python CLI tools, cross-platform apps, and Linux/Unix applications using platformdirs.
npx claudepluginhub jamie-bitflight/claude_skills --plugin xdg-base-directoryHow this skill is triggered — by the user, by Claude, or both
Slash command
/xdg-base-directory:xdg-base-directoryThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Implement standardized directory paths for configuration, data, cache, and state files following the XDG Base Directory Specification.
Resolves the base directory for CAFleet output files (scratch, audit, figures) via a deterministic CLI + user-question fallback. Consumed by other skills, not invoked directly.
Generates professional bash/shell scripts with Google Shell Style Guide compliance, ShellCheck validation, cross-platform support (Linux/macOS/Windows/containers), POSIX compliance, security hardening, error handling, performance optimization, and BATS testing. Useful for automation, DevOps/CI/CD, build scripts, debugging.
Provides shell scripting expertise for bash, zsh, POSIX; CLI tools like jq, yq, fd, rg for JSON/YAML processing, file search, automation, error handling, and cross-platform best practices. Useful for CLI commands, pipes, script development.
Share bugs, ideas, or general feedback.
Implement standardized directory paths for configuration, data, cache, and state files following the XDG Base Directory Specification.
Use this skill when:
~/.appname to XDG-compliant pathsThe XDG Base Directory Specification (version 0.8, May 2021) defines these critical requirements:
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
When an environment variable is unset or empty, use the specification-defined default value.
$XDG_CONFIG_HOME, $XDG_DATA_HOME) take precedence$XDG_CONFIG_DIRS, $XDG_DATA_DIRS) searched in order$XDG_DATA_DIRS and $XDG_CONFIG_DIRS use colon (:) as path separator, similar to $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 |
| Variable | Purpose | Default |
|---|---|---|
$XDG_DATA_DIRS | System data search path | /usr/local/share/:/usr/share/ |
$XDG_CONFIG_DIRS | System config search path | /etc/xdg |
| Path | Purpose |
|---|---|
$HOME/.local/bin | User-specific executable files |
Note: Distributions should ensure $HOME/.local/bin appears in $PATH.
Choose the appropriate directory based on data characteristics:
Use $XDG_CONFIG_HOME for:
~/.config/myapp/config.tomlUse $XDG_DATA_HOME for:
~/.local/share/myapp/models/model.ggufUse $XDG_STATE_HOME for:
~/.local/state/myapp/history.logUse $XDG_CACHE_HOME for:
~/.cache/myapp/downloads/Use $XDG_RUNTIME_DIR for:
/run/user/1000/myapp/socketUse 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')]
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)
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:
~/Library, Windows %APPDATA%)ensure_exists=TrueWhen to use stdlib-only:
~/.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'
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'
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
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)
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'
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'
XDG_RUNTIME_DIRProblem: $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'
# 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/
"""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')]
~/.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
For TOML configuration files with XDG support, activate the toml-python skill:
Skill(skill: "python3-development: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.
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$XDG_RUNTIME_DIR (tmpfs limits)$XDG_RUNTIME_DIR has no default; check for None