Day 26 — HM files & XDG
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:
- File contents (or a path) enter the Nix store as part of the HM generation
- Activation creates symlinks from the home directory into the store (or manages mutable copies depending on options)
- 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
mkOutOfStoreSymlinkpatterns (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.txtTheory 6 — Theming (optional, bounded)
Theming (GTK, fonts, pointer cursors) is infinite rabbit hole. If you care:
- Prefer HM modules that exist (
gtk,qt,fonts.fontconfigat 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 -50Pick 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 errorRestore 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.