Day 26 — HM files & XDG

Updated

July 30, 2026

Day 26 — HM files & XDG

Stage III · ~4h
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 day exists

Day 25 enabled programs modules. Day 26 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 (Stage IV) for sensitive values

Theory 2 — home.file vs xdg.configFile

API Default target idea Example use
home.file."<path>" Relative to $HOME home.file.".npmrc", home.file.".config/acme/x"
xdg.configFile."<path>" Under XDG config home (usually ~/.config) xdg.configFile."foot/foot.ini"
xdg.dataFile / xdg.mimeApps Data / MIME Desktop entries, 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 bit .executable = true when needed

Repo layout option:

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

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


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.


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 (Stage IV)
Public config Store-backed symlink is fine

Check links:

ls -la ~/.config/demo
readlink -f ~/.config/demo/hello.txt

Theory 6 — Theming (optional, bounded)

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

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

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


Theory 7 — Multi-machine user splits (pattern)

Same user, different hosts:

# homes/alice/default.nix
{ lib, ... }:
{
  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.


Worked example — Migrate a real git ignore + editor config

# homes/alice/config/git-ignore-global
# *.env
# .direnv/
# result

# 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.


Lab 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).


Lab 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.


Lab 3 — Migrate via source

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


Lab 4 — Conflict drill

# while HM is managing a path, try creating a conflicting real file after backup
# e.g. remove link, replace with a regular file, rebuild, observe activation error

Restore by removing the blocker and rebuilding. Document the error text for future you.


Lab 5 — Secret anti-pattern (do not commit real secrets)

Create a fake example only on a branch you will not push, or purely local:

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

Show yourself:

# after a bad activation, store path is readable
# cat $(readlink -f ~/.config/acme/token)

Then delete the pattern, rebuild, and write: “secrets need Stage IV tooling.”

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


Lab 6 — Structure the home tree

homes/alice/
  default.nix      # imports
  programs.nix     # git, bash, ...
  files.nix        # xdg / home.file
  config/          # source files

Rebuild once more to prove the split.


Common gotchas

Symptom / mistake What to do
Activation fails on existing file Backup/remove blocker; rebuild
Edited ~ file, change vanished You edited a symlink target 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

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

Commit

git add .
git commit -m "day26: HM files and XDG migrations"

Write three personal gotchas before continuing.


Tomorrow

Day 27 — HM user services: user-level systemd services and timers for work that should not be system daemons.