Home files and XDG

Updated

July 30, 2026

Home files and XDG

Goal: Manage real user files with Home Manager—home.file, xdg.configFile, and XDG directories—while understanding store-backed links, permissions, and what must never be checked into the flake as plaintext secrets.

Why this chapter exists

The Home Manager chapter enabled programs modules. This chapter is the file layer: most of a personal environment is text under ~/.config, ~/.local, and a few classic dotfiles. HM makes those files declarative and rebuildable—or, if misused, makes secrets world-readable in the Nix store.


Theory 1 — How HM places files

Typical mechanism:

  1. File contents (or a path) enter the Nix store as part of the HM generation
  2. Activation creates symlinks from the home directory into the store (or manages mutable copies depending on options)
  3. Next activation may replace links when content changes

Implications:

Property Consequence
Store paths are world-readable No passwords, tokens, private keys in HM file content
Links point at immutable content Editing the live file in ~ may be wrong layer—edit the flake
Rebuild required for changes Dotfile tweak → rebuild (or temporary escape hatch)

Escape hatches (know, use sparingly)

  • Manual files outside HM for truly mutable scratch
  • mkOutOfStoreSymlink patterns (advanced; can reintroduce impurity)
  • Secrets tooling (services/security part) for sensitive values
# advanced awareness only — impure path outside store
# home.file."somewhere".source =
#   config.lib.file.mkOutOfStoreSymlink "/home/alice/mutable/config";

Prefer store-backed management for anything you want rebuildable and reviewable in git.


Theory 2 — home.file vs xdg.configFile

API Default target idea Example use
home.file."<path>" Relative to $HOME home.file.".npmrc"
xdg.configFile."<path>" Under XDG config home (usually ~/.config) xdg.configFile."foot/foot.ini"
xdg.dataFile Under data home Desktop entries, shared data
xdg.mimeApps MIME defaults Default apps

Prefer xdg.configFile for modern tools that honor XDG.

Minimal examples

# homes/alice/files.nix
{ pkgs, ... }:
{
  xdg.enable = true;

  xdg.configFile."demo/hello.txt".text = ''
    managed by home-manager
    rebuilt with the host flake
  '';

  home.file.".config/demo/from-home-file.txt".text = ''
    same idea via home.file
  '';
}

Import from homes/alice/default.nix:

{
  imports = [ ./files.nix ];
}

Theory 3 — Content sources: text, source, templates

Field Use
.text Inline small configs
.source Path to a file in the repo (./config/gitconfig sibling)
.source = pkgs.writeText ... Generated in Nix
.executable = true Scripts that need +x

Repo layout option:

homes/alice/
  default.nix
  files.nix
  config/
    starship.toml
    htoprc
    nvim/init.lua
xdg.configFile."starship.toml".source = ./config/starship.toml;
xdg.configFile."nvim/init.lua".source = ./config/nvim/init.lua;

Benefit of source: normal editor diffs on real files; Nix only wires them.

Recursive directories

xdg.configFile."nvim" = {
  source = ./config/nvim;
  recursive = true;
};

Use when a tool expects a tree. Keep secrets out of that tree.


Theory 4 — XDG directories

xdg.enable = true;
# xdg.configHome / dataHome / cacheHome / stateHome can be customized
# defaults are usually correct — only override with a reason
Variable Typical path
XDG_CONFIG_HOME ~/.config
XDG_DATA_HOME ~/.local/share
XDG_CACHE_HOME ~/.cache
XDG_STATE_HOME ~/.local/state

Tools differ in XDG discipline. Your job: put managed files where the tool reads them, not where tradition says.

Classic vs XDG

Classic path Prefer if tool supports XDG
~/.gitconfig programs.git (HM) or XDG git config
~/.bashrc programs.bash
~/.config/foo xdg.configFile."foo/..."

Theory 5 — Permissions and clobber behavior

HM will not always happily overwrite unexpected pre-existing files—activation may fail if a real file blocks a symlink.

Situation Approach
Old imperative dotfile in the way Backup and remove, then rebuild
You need a secret file mode 0600 Do not put secret content in store; use runtime secret install
Public config Store-backed symlink is fine

Check links:

ls -la ~/.config/demo
readlink -f ~/.config/demo/hello.txt
stat "$(readlink -f ~/.config/demo/hello.txt)"

Mode options

home.file.".local/bin/lab-tool" = {
  text = ''
    #!/usr/bin/env bash
    echo lab
  '';
  executable = true;
};

Remember: store object readability still applies to content; executable does not make content private.


Theory 6 — Theming (optional, bounded)

Theming (GTK, fonts, pointer cursors) is an infinite rabbit hole. If you care:

  • Prefer HM modules that exist (gtk, qt, fonts at system level, etc.)
  • Bound the experiment: one terminal theme + one font—not a full desktop redesign

If you run a server-only lab: skip theming; migrate shell/git configs instead.


Theory 7 — Multi-machine user splits

Same user, different hosts:

# homes/alice/default.nix
{
  imports = [
    ./common.nix
    # ./hosts/lab.nix   # optional per-host HM slice
  ];
}

Or select in the flake:

home-manager.users.alice = {
  imports = [
    ./homes/alice/common.nix
    ./homes/alice/hosts/lab.nix
  ];
};

Do not copy entire homes per host if 90% is shared—common + overlay modules.


Theory 8 — programs.* vs raw files for the same tool

Prefer When
programs.git Structured options exist
xdg.configFile Tool has no HM module or you want raw file fidelity
Both for same path Conflict risk—pick one
# Good: structured
programs.git.ignores = [ "result" ".direnv/" ];

# Also good: raw when complex
# xdg.configFile."git/ignore".source = ./config/git-ignore-global;

Theory 9 — What never goes in managed files

Never in flake/HM text Why
API tokens Store world-readable
Private SSH keys Same + git leak
Cloud provider secrets Same
DB passwords Same
.env production values History forever if committed

Gray area: non-secret feature flags, public keys, preferred editor settings—OK.


Worked example — Migrate git ignores + editor config

# homes/alice/git.nix
{
  programs.git = {
    enable = true;
    userName = "Alice";
    userEmail = "alice@example.invalid";
    ignores = [
      "*.env"
      ".direnv/"
      "result"
      "result-*"
    ];
  };

  xdg.configFile."nvim/init.lua".text = ''
    -- minimal managed nvim config
    vim.opt.number = true
    vim.opt.relativenumber = true
  '';
}

No API tokens. No extraConfig with github.token.


Worked example — Structured home tree

homes/alice/
  default.nix
  programs.nix
  files.nix
  config/
    starship.toml
    htop/htoprc
# homes/alice/files.nix
{
  xdg.enable = true;
  xdg.configFile."starship.toml".source = ./config/starship.toml;
  xdg.configFile."htop/htoprc".source = ./config/htop/htoprc;
}

Exercises

Exercise 1 — Inventory live dotfiles

As your lab user:

ls -la ~ | head -50
ls -la ~/.config 2>/dev/null | head -50

Pick 3–5 real configs to migrate (git, shell rc fragments, one tool). Skip secrets and browser profiles.

Journal: path → HM attribute choice (programs.* vs xdg.configFile vs home.file).

Exercise 2 — First managed XDG file

Add xdg.enable = true and a demo xdg.configFile. Rebuild. Verify symlink into store:

ls -la ~/.config/demo
readlink -f ~/.config/demo/hello.txt
stat "$(readlink -f ~/.config/demo/hello.txt)"

Note permissions on the store path (world-readable). That observation is the point.

Exercise 3 — Migrate via source

Place a real config file next to the HM module and set .source. Rebuild. Confirm tool behavior.

Exercise 4 — Conflict drill

While HM manages a path: backup, replace symlink with a regular file, rebuild, observe activation error. Restore by removing the blocker and rebuilding. Document the error text.

Exercise 5 — Secret anti-pattern (fake only)

On a local throwaway branch (do not push real secrets):

# BAD — educational only, use fake value
# home.file.".config/acme/token".text = "super-secret-placeholder";

If you activate a fake value, show yourself:

# cat "$(readlink -f ~/.config/acme/token)"

Then delete the pattern, rebuild, and write: “secrets need secrets tooling, not HM file content.”

Never commit real tokens. If you did: rotate them.

Exercise 6 — Structure the home tree

homes/alice/
  default.nix
  programs.nix
  files.nix
  config/

Rebuild once more to prove the split.

Exercise 7 — Recursive config tree

If you use nvim or similar, try recursive = true on a config directory. Confirm links.

Exercise 8 — Executable home file

Install a tiny script under ~/.local/bin via home.file + executable = true. Ensure ~/.local/bin is on PATH (HM home.sessionPath or shell config).

Exercise 9 — programs vs file for one tool

For git or another tool, write two sentences: why you chose programs.* vs raw file.

Exercise 10 — Multi-host plan

Sketch common.nix + optional hosts/lab.nix even if you only have one machine.

Exercise 11 — Cache vs config

List one path that should stay unmanaged (cache, browser profile). Confirm it is not in the flake.

Exercise 12 — Activation after migrate

sudo nixos-rebuild switch --flake .#lab
journalctl -u home-manager-$USER.service -b --no-pager | tail -40

Exercise 13 — Documentation

Add a short “Home files” section to docs/layout.md: which configs are managed, which are intentionally not.


Common gotchas

Symptom / mistake What to do
Activation fails on existing file Backup/remove blocker; rebuild
Edited ~ file, change vanished You edited a symlink or lost it on rebuild—edit flake
Secret in store Remove from Nix; rotate credential
Wrong path (~/.foo vs XDG) Check tool docs; fix attribute
Huge binary blobs in HM Don’t; keep media out of the flake
text with nested quotes hell Prefer .source files
Assuming 0600 store file is private Store is not a private vault
Dual management of same path Pick programs.* or file API
Recursive source includes .git junk Clean source tree
Theming rabbit hole on server Skip; ship shell/git first

Checkpoint

  • xdg.enable (or conscious decision not to)
  • At least two real configs managed via HM files or programs
  • Can show a managed path is store-backed
  • Home tree split into programs/files modules
  • Written note: no secrets in HM file content
  • Conflict + recovery once
  • At least one .source migration
  • Know when to use escape hatches vs store links

Journal (optional)

Write three personal gotchas before continuing.