Use the XDG Base Directory Specification!

Why?

Look at your home

$ exa -al --no-filesize --no-user --no-time --no-permissions --color=never ~
.alsoftrc -> /home/edwin/.dots/user/.alsoftrc
.android/
.ansible/
.aqbanking/
.audacity-data/
.bash_logout -> /home/edwin/.dots/user/.bash_logout
.bash_profile -> /home/edwin/.dots/user/.bash_profile
.bashrc -> /home/edwin/config/bash/bashrc.sh
.cache/
.choosenim/
.code-d/
.config/
config/
.cpan/
data/
Desk/
Dls -> /storage/edwin/Dls/
Docs -> /storage/edwin/Docs/
.eclipse/
.elementary/
.emacs.d/
.emulator_console_auth_token
.flutter
.flutter_tool_state
.gem/
.ghc/
.ghidra/
.gnome/
.gnupg/
.godot/
.gore/
.gradle/
.gsutil/
.guestfish
.heekscad
.helioslauncher/
HomeBank-20210521.bak
HomeBank-20210607.bak
HomeBank-20210611.bak
.hushlogin -> /home/edwin/.dots/user/.hushlogin
.idapro/
.ivy2/
.java/
javasharedresources/
.kb/
.kube/
.lldb/
.local/
.lunarclient/
.lyxauth
.m2/
macOS.vdi
.mcfly/
.metals/
.minecraft/
.mono/
.mozilla -> /storage/data/mozilla/
.mputils/
.mume/
Music -> /storage/edwin/Music/
.omnisharp/
.ort/
.osc_cookiejar
.pack/
.paradoxlauncher/
.parsec/
Pics -> /storage/edwin/Pics/
.pki/
.pm2/
.profile -> /home/edwin/config/profile/profile.sh
.pythonhist
.sbt/
.scim/
.ssh -> /storage/data/ssh/
.steam/
.steampath -> /home/edwin/.steam/sdk32/steam
.steampid -> /home/edwin/.steam/steam.pid
.step/
.subversion/
.swt/
.tooling/
'Universe Sandbox'/
Vids -> /storage/edwin/Vids/
.vscode/
.w3m/
.wine/
.xinitrc -> /home/edwin/config/X11/xinitrc*
.yarnrc -> /home/edwin/.dots/user/.yarnrc
.zcompdump
.zoom/
.zshenv -> /home/edwin/config/zsh/.zshenv

What if your house was clean and tidy?

$ exa -al --no-filesize --no-user --no-time --no-permissions --color=never ~
.cache/
.config/
Desk/
Dls
Docs
.local/
Music
Pics
.profile
Vids

End users of your application (and hopefully yourself, as well) want a clean home directory. Instead of having files like ~/.gitconfig scattered chaotically in the home folder, it would be better for them to reside in a dedicated configuration directory

I like how Filip from 0x46.net explained the issue from the perspective of a user:

My own home directory contains 25 ordinary files and 144 hidden files. The dotfiles contain data that doesn't belong to me: it belongs to the programmers whose programs decided to hijack the primary location designed as a storage for my personal files. I can't place those dotfiles anywhere else and they will appear again if I try to delete them. All I can do is sit here knowing that in the darkness, behind the scenes, they are there. . . . I dread the day in which I will hear a loud knock on my door and one of those programmers will barge in informing me that he is going to store a piece of his furniture in the middle of my living room, If I don't mind.

Filip from 0x46.net

Succinctly, following the spec means (by the very least) your application files are nearly categorized in subdirectories of the home folder. For instance, locations of cache files default to ~/.cache while locations of config files default to ~/.config, etc.

Benefits

If you aren't already sold on a clean home directory, there are additional benefits

As a more concrete example, imagine that /etc did not exist - configuration would be chaotically scattered about in a way similar to how $HOME exists today. But, because we have /etc specifically designated as a directory for config files (along with other directories) by the File Hierarchy Standard, it is significantly easier to locate, edit, and backup system files across machines. Those ergonomics should not just exist for system-level files, but for the user-level as well.

Thus, I implore, I beg all you application developers to implement the XDG Base Directory Specification

How?

First, categorize the files that your program needs to write into four categories:

  1. Configuration
  2. Configuration files that affect the behavior of an program. Even if the configuration file is not meant to be human-editable, it likely belongs here.

  3. Data
  4. General files and data that is inherently portable across computers. Examples include fonts, files downloaded from the internet, and desktop entries.

  5. State
  6. Files that hold the state of the application. This may include logs, command history, recently used files, and game save data. In other words, if the data is unique for a given machine, the file belongs here.

  7. Cache
  8. Cache files.

Each aforementioned category is mapped to a special environment variable, which is the directory that contains all files of that category. For example, "Configuration" files are all stored in $XDG_CONFIG_HOME. If that environment variable is invalid, then the default value of $HOME/.config should be used instead. See the full table below

Category Environment Variable Default Value FHS Approximation
Configuration $XDG_CONFIG_HOME $HOME/.config /etc
Data $XDG_DATA_HOME $HOME/.local/share /usr/share
State $XDG_STATE_HOME $HOME/.local/state /var/lib
Cache $XDG_CACHE_HOME $HOME/.cache /var/cache

There are three ways the environment variable can be invalid

  1. If it is unset (unset XDG_CONFIG_HOME)
  2. If it is set to an empty value (XDG_CONFIG_HOME="")
  3. If it is not an absolute path (XDG_CONFIG_HOME="./config")

Code examples

Now that you're acquainted with the standard, you probably want to sees some code examples. The following programs are for Linux only (See the FAQ below for details on MacOS and Windows). Of course, if you are actually writing a program, I recommend using a library

The following is Go 1.13+ code. Note that it does not use os.UserConfigDir (Source) since that does not silently ignore non-absolute paths as per the spec

Show Go
package main
import (
  "os"
  "fmt"
  "log"
  "path/filepath"
)

func getConfigDir() (string, error) {
  configDir := os.Getenv("XDG_CONFIG_HOME")

  // If the value of the environment variable is unset, empty, or not an absolute path, use the default
  if configDir == "" || configDir[0:1] != "/" {
    homeDir, err := os.UserHomeDir()
    if err != nil {
      return "", err
    }
    return filepath.Join(homeDir, ".config", "my-application-name"), nil
  }

  // The value of the environment variable is valid; use it
  return filepath.Join(configDir, "my-application-name"), nil
}

func main() {
  config_dir, err := getConfigDir()
  if err != nil {
    panic(err)
  }

  fmt.Println(config_dir)
}

The following is Python 3.5+ code. It uses Path.home (Source) (which uses os.path.expanduser (Source))

Show Python
import sys, os
from pathlib import Path

def get_config_dir() -> str:
  config_dir = os.getenv('XDG_CONFIG_HOME', '')

  // If the value of the environment variable is unset, empty, or not an absolute path, use the default
  if config_dir == '' or config_dir[0] != '/':
    return str(Path.home().joinpath('.config', 'my-application-name'))

  // The value of the environment variable is valid; use it
  return str(Path(config_dir).joinpath('my-application-name'))

config_dir = get_config_dir()
print(config_dir)

The following is Bash code. Note that it does not use "${XDG_CONFIG_HOME:-$HOME/.config}" since that does not silently ignore non-absolute paths as per the spec

Show Bash
get_config_dir() {
  unset REPLY; REPLY=

  local config_dir="$XDG_CONFIG_HOME"

  # If the value of the environment variable is unset, empty or not an absolute path, use the default
  if [ -z "$config_dir" ] || [ "${config_dir::1}" != '/' ]; then
    REPLY="$HOME/.config/my-application-name"
    return
  fi

  # The value of the environment variable is valid; use it
  REPLY="$config_dir/my-application-name"
}

get_config_dir
printf '%s\n' "$REPLY"

FAQ

Is that it?

There is more to the standard, but you should know and implement at least the aforementioned, which is the most important part. If you don't want to implement this yourself, use a library. By implementing this, your users will silently thank you!

What if I already do the wrong thing and use the home folder directly?

If you write to a few individual files in $HOME, simply check if those files exist before following the XDG Base Directory Specification. Consider the following example, given a traditional config location of ~/foo-app.json: If ~/foo-app.json exists use it; if it does not exist, then check if $XDG_CONFIG_HOME is set and is valid. If it is, then write to $XDG_CONFIG_HOME/foo-app/foo-app.json. If not, then write to $HOME/.config/foo-app/foo-app.json

However if you hold all configuration and data inside a subdirectory of home (ex. ~/.ansible), things are a bit less clear-cut. Moving everything to $XDG_STATE_HOME/application-dir would be easiest to the application developer, but less semantically correct. If you don't want to worry about migrating multiple files to separate directories, it would be a good first start to make the location of the directory configurable by an environment variable. For example, Terraform exposes this option using TF_DATA_DIR. Even just exposing the directory through an environment variable is incredibly useful

Who else does this?

KittyFontForgeImageMagickAlacrittyComposerTerminatorclangdchezmoiaria2bati3nanopicompoetryVLCawesomeaercmicroHandbrakeOfflineIMAPpolybarrclonexmonadmesaGodotDockerAnkihttpiecitrabasherasunderTransmissionhtopTermiteGitKakouneBlenderGstreamerrangerPryTypeScriptnavid-feetMercurialLibreOfficeAudaciousbyobucolordiffcmusccacheantimicrolftpmccalcurseDelugeTerrariaWiresharkswayxsettingsdtmuxPulseAudioneomuttVirtualBoxbroothttpieALSApandocnbtigcargoopenboxasdffishfontconfigDolphinMultiMCpnpmGIMPbinwalkInkscapeiwdmpvjosmWechat

See the Arch Wiki for a longer list and more details

I already provide a --config option. Do I really need this?

Absolutely! The ability to specify files or directories with a command-line argument cannot consistently be used. For example, you could define a shell alias, but it won't always be used since the Bash option expand_aliases is unset by default in a non-interactive environment. Furthermore, if your program is invoked in any programmatic way (ex. an exec style syscall), there is no reasonable way to ensure that flag is passed.

Should I do this on macOS?

I've done quite a bit of research, and there doesn't seem to be a common consensus. Generally speaking, if your program's only interface is a CLI, then it's permissible to follow the XDG Base Directory Specification, even if you aren't on Linux. This seems to be pretty common across the ecosystem, especially for Bash programs. On the other hand, if it's a GUI, then you would usually follow the standard macOS directory locations. For example, Sublime Text 3 stores it's preferences on macOS in ~/Library/Application Support/Sublime Text 3/Packages/User, even if it also ships with a subl command.

Should I do this on Windows?

I don't have a straight answer for you, simply because I don't use Windows frequently enough. I'll add that Windows already has a preexisting directory to place user data. Notwithstanding this fact, widely-used applications like scoop follow the spec for configuration. Another command line app, PSKoans hard-codes ~/.config. Admittedly, its usage is not widely prevalent, especially compared to macOS, so the answer would be closer to a "no"

Who are you?

I'm Edwin Kofler, a software developer that wants to proliferate the knowledge (and implementation) of this specification to a wider group of developers. I've opened issues (that are now fixed) in repositories such as Poetry and pnpm, and have proposed PRs (that are now merged) in repositories such as osc, vscode-kubernetes-tools, mume, basher, and more! Together, I hope we can make the home directory clean again!

Auxillary resources