Home files and XDG
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:
- 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 (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 -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).
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 -40Exercise 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
.sourcemigration - Know when to use escape hatches vs store links
Journal (optional)
Write three personal gotchas before continuing.