Day 9 — Flake Layout, Lock Management & Home Manager
Day 9 — Flake Layout, Lock Management & Home Manager
Day 23 — Flake layout
Stage III · ~4h
Goal: Refactor a Stage II single-host flake into a hosts / modules / homes layout that stays rebuildable, readable, and ready for multi-host growth—without a 2 000-line flake.nix.
Baseline: NixOS / nixpkgs 26.05, flakes, modern nix CLI. Home Manager lands on Day 25; today only create the homes/ slot so later imports have a home.
Why this day exists
Stage II proved you can own one machine from a flake. Stage III is about shape: the difference between a lab notebook and a repo you can still navigate in six months—or hand to a second host.
A common failure mode:
flake.nix # 1800 lines of mixed host, packages, users, networking
hardware-configuration.nix
That works until the second machine, the second user, or the first “extract this SSH policy” refactor. Layout is not aesthetics; it is dependency direction and import graph hygiene.
Theory 1 — What “layout” means in a flake monorepo
A flake has two layers:
| Layer | Role |
|---|---|
flake.nix outputs |
Thin wiring: inputs → nixosConfigurations, later devShells / checks / packages |
| Module tree | Almost all real config: options + config fragments imported by hosts |
Rule of thumb: if a file is only true for one machine, it lives under hosts/<name>/. If it is true for a role or policy, it lives under modules/. If it is true for a user, it will live under homes/ (HM starts Day 25).
Recommended tree (target for today)
flake.nix
flake.lock
hosts/
lab/
default.nix # host entry: imports modules + hardware
hardware-configuration.nix
modules/
common/
default.nix # baseline shared by all hosts
nix.nix # nix settings, flakes, gc policy (optional split)
users.nix
ssh.nix
networking.nix
roles/
workstation.nix # optional role
server.nix
services/
# empty or one tiny service extracted from Stage II
homes/
alice/
default.nix # stub for Day 25
lib/
default.nix # optional helpers (mkHost, etc.)
Names are conventions, not law. The invariant is layering, not exact folder names.
Theory 2 — Dependency direction (prevent circular imports)
Think of imports as a DAG:
flake.nix
→ hosts/lab
→ modules/common/*
→ modules/roles/*
→ modules/services/*
→ hardware-configuration
→ (later) homes via HM module
| Allowed | Forbidden |
|---|---|
| Host imports modules | Module imports a specific host |
| Role imports common modules | Common imports role |
| Service module is self-contained | Service module reaches into hosts/lab paths |
lib pure helpers used by modules |
lib that evaluates full NixOS configs |
Circular imports fail evaluation with confusing “infinite recursion” or “attribute missing” symptoms. Design the graph before you split files blindly.
What belongs where
| Concern | Place |
|---|---|
networking.hostName, hardware, disk labels |
hosts/<name>/ |
| SSH policy, firewall defaults, time zone, locale | modules/common/ |
| “This is a headless server” package set | modules/roles/server.nix |
| One service option bundle | modules/services/<name>.nix |
| User shell, git, editors | homes/<user>/ (Day 25+) |
specialArgs, input plumbing |
flake.nix only |
Theory 3 — Host entry pattern
A host file is a module, not a second flake:
# hosts/lab/default.nix
{ config, pkgs, lib, ... }:
{
imports = [
./hardware-configuration.nix
../../modules/common
../../modules/roles/server.nix
# ../../modules/services/my-timer.nix
];
networking.hostName = "lab";
# Host-only overrides (mkForce / mkDefault as needed)
# services.openssh.openFirewall = true; # if you keep SSH on lab LAN
}And the flake stays thin:
# flake.nix (sketch — adjust system and input names to your Stage II flake)
{
description = "90DaysOfX NixOS lab — Stage III layout";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
};
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux"; # or aarch64-linux
in {
nixosConfigurations.lab = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./hosts/lab
];
# specialArgs = { inherit inputs; }; # when you need inputs in modules
};
};
}default.nix vs explicit path
Importing ./hosts/lab loads ./hosts/lab/default.nix. Prefer that over listing every leaf in flake.nix.
Theory 4 — Module “barrel” files
modules/common/default.nix re-exports fragments:
# modules/common/default.nix
{
imports = [
./nix.nix
./users.nix
./ssh.nix
./networking.nix
];
}Benefits:
- Hosts import one common entry
- You can still open a single policy file for review
- Diffs stay small when only SSH changes
Refactor behavior-preserving first: cut → import → rebuild → only then change options. Never mix layout moves with hardening or new services in one commit.
Theory 5 — Hardware config and purity
hardware-configuration.nix is host-local (UUIDs, firmware). Do not share it across machines. Optional lib/mkHost helpers can wait until you have three hosts—prefer an obvious explicit nixosSystem until then.
Before → after: Stage II points flake.nix at a monolith configuration.nix; Stage III points at ./hosts/lab, which imports hardware + modules/common (+ roles/services). Same behavior; smaller change surface.
Lab 1 — Inventory the monolith
Suggested workspace: ~/lab/90daysofx/02-nixos/day23 (or your real flake repo).
cd /path/to/your-flake
find . -name '*.nix' | sort
wc -l flake.nix configuration.nix hosts/*/*.nix 2>/dev/nullIn your journal:
- List every concern in the big file (users, SSH, packages, networking, custom module, …)
- Mark each as host / common / role / service / user
- Note any absolute paths or secrets (flag secrets for Stage IV—do not leave plaintext passwords in git)
Lab 2 — Create the directory skeleton
mkdir -p hosts/lab modules/common modules/roles modules/services homes/alice libMove hardware config:
# adjust names to your Stage II layout
mv hardware-configuration.nix hosts/lab/
# if configuration.nix was the monolith, keep a copy for reference
cp configuration.nix /tmp/configuration.nix.bakLab 3 — Split common modules
Create focused files. Example SSH fragment:
# modules/common/ssh.nix
{ lib, ... }:
{
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = "no";
};
};
}Users (replace with your Stage II usernames/keys—keys only, no password hashes in examples here):
# modules/common/users.nix
{ pkgs, ... }:
{
users.users.alice = {
isNormalUser = true;
extraGroups = [ "wheel" ];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAA...REPLACE_WITH_YOUR_PUBLIC_KEY alice@lab"
];
};
# security.sudo.wheelNeedsPassword = true; # your policy
}Nix settings:
# modules/common/nix.nix
{
nix.settings = {
experimental-features = [ "nix-command" "flakes" ];
# auto-optimise-store = true; # optional
};
nixpkgs.config.allowUnfree = false; # be explicit about policy
}Barrel:
# modules/common/default.nix
{
imports = [
./nix.nix
./users.nix
./ssh.nix
./networking.nix
];
}Host entry:
# hosts/lab/default.nix
{ ... }:
{
imports = [
./hardware-configuration.nix
../../modules/common
];
networking.hostName = "lab";
system.stateVersion = "26.05"; # keep your real stateVersion
}Point flake.nix at ./hosts/lab only.
Lab 4 — Rebuild and prove equivalence
# dry evaluation / build first if you prefer
nixos-rebuild build --flake .#lab
# then switch on the lab host
sudo nixos-rebuild switch --flake .#labVerify:
hostname
systemctl is-active sshd
nixos-versionIf something broke: roll back (Day 13 muscle memory), fix one module, retry.
Lab 5 — Document the tree
Write docs/layout.md or a short journal entry:
flake.nix → outputs only
hosts/lab → hostname, hardware, host overrides
modules/common → shared policy
modules/roles → (empty or one role)
modules/services → extracted Stage II service if any
homes/alice → Day 25
Commit the layout as its own commit (message below). Optional stretch: sketch hosts/spare/ reusing modules/common but do not wire a non-bootable nixosConfigurations entry without real hardware.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Infinite recursion after split | Module imports a host or config cycle; straighten the DAG |
error: path '…' does not exist |
Relative paths wrong after move; re-check ../../modules |
Flake still points at old configuration.nix |
Update modules = [ ./hosts/lab ]; |
| Hardware UUIDs missing | You moved hardware file but forgot to import it |
| “Works on eval, fails on boot” | Bootloader/filesystem only in hardware host file—verify |
Mixed absolute /home/you/... imports |
Use flake-relative paths only |
| Giant PR of layout + features | Split commits: move-only first |
Checkpoint
- Directory tree matches hosts / modules / homes (homes may be stub)
flake.nixis thin wiring, not the bulk of config
nixos-rebuild switch --flake .#lab(or your hostname) succeeds
- Import graph has no host→module reverse deps
- Layout documented in a short tree note
- Behavior roughly matches pre-refactor Stage II host
Commit
git add .
git commit -m "day23: flake layout hosts/modules/homes"Write three personal gotchas before continuing.
Tomorrow
Day 24 — Lock discipline: selective flake update, reading lock diffs, and pinning strategy so “layout clean” does not become “nixpkgs random walk.”
Day 24 — Lock discipline
Stage III · ~4h
Goal: Treat flake.lock as a deliberate pin, not a mysterious generated blob—update one input at a time, read diffs, and explain how locks interact with registries and multi-machine rebuilds.
Blind nix flake update on a production host is how “nothing changed except everything.” Prefer staged, selective updates.
Why this day exists
Flakes solve channel drift only if you respect the lock file. Teams fail when:
- Someone updates all inputs “because CI was green last week”
- Lock files are gitignored (never do this for apps/hosts)
- Two machines use different locks and argue about “Nix being unreproducible”
- Registries silently redirect
nixpkgsaway from the flake’s pin
Today is ops hygiene for every later day that adds inputs (Home Manager, sops-nix, …).
Theory 1 — What flake.lock actually pins
For each flake input, the lock records roughly:
| Field | Meaning |
|---|---|
type / owner / repo / url |
Where the input came from |
rev |
Exact git commit (or equivalent) |
narHash |
Content hash of the fetched tree |
lastModified |
Metadata for humans/tools |
Implication: two clones with the same lock should fetch the same input trees (modulo substituted vs rebuilt outputs later). Without the lock, nixos-26.05 is a moving branch tip.
Inputs vs follow-ons
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
home-manager.url = "github:nix-community/home-manager/release-26.05";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
};follows means Home Manager reuses your nixpkgs pin instead of its own nested lock entry as a separate copy. That reduces “two nixpkgs versions in one eval” confusion and often saves eval work.
Theory 2 — Update operations (precision toolkit)
| Command | Effect |
|---|---|
nix flake lock |
Refresh lock metadata without changing intended refs (often no-op if already locked) |
nix flake update |
Update all inputs to latest matching their url refs |
nix flake update nixpkgs |
Update only the nixpkgs input |
nix flake update home-manager |
Update only that input |
nix flake metadata |
Show resolved pins / last modified |
Modern CLI also supports updating multiple named inputs; check nix flake update --help on your Nix version.
Mental model
flake.nix → policy ("track nixos-26.05")
flake.lock → decision ("this commit of that policy")
Changing flake.nix URLs without updating lock can error or keep stale locks depending on change type—always re-lock intentionally and commit both files together when inputs change.
Theory 3 — How to read a lock diff
git diff flake.lock | head -200Look for:
- Which input changed (
"nixpkgs": {block)
- Old
revvs newrev
narHashchange (expected when rev changes)
- Nested inputs (if not
follows)
Optional deep inspection:
nix flake metadata
nix flake metadata --json | jq '.locks.nodes.nixpkgs'Map rev to a GitHub compare URL when investigating regressions:
https://github.com/NixOS/nixpkgs/compare/<old>...<new>
Theory 4 — Pin strategies (pick a house rule)
| Strategy | When | Risk |
|---|---|---|
Track release branch (nixos-26.05) + lock commits |
Default for this book | Branch tip moves; you choose when to update |
Pin exact rev in flake.nix |
Freeze hard for a milestone | Manual bumps; easy to forget security updates |
Track nixos-unstable |
Bleeding edge lab | Breakage rate higher |
| Multiple flakes (host vs tools) | Advanced separation | Operational complexity |
Recommended for 90DaysOfX lab: track nixos-26.05 (and matching HM release), commit lock, update one input per session, rebuild + smoke-test before updating the next.
Security vs stability
Updating nixpkgs is how you receive package and module fixes. Never freeze a public-facing host forever “because it works.” Discipline is when and how, not whether.
Theory 5 — Registries (light literacy)
Flake registries map short names (nixpkgs) to URLs for ad-hoc commands like nix run nixpkgs#hello.
| Concern | Practice |
|---|---|
| Global registry | Convenient for exploration |
| Your host flake | Should use explicit inputs, not hope the registry matches |
| CI | Prefer locked flake in repo over registry luck |
nix registry listIf global nixpkgs points at unstable while your flake pins 26.05, that is fine for one-off nix run—as long as you never assume those are the same pin.
Theory 6 — Multi-machine and “reproducible”
Same lock + same system (x86_64-linux) → same evaluation of inputs. Realization may still:
- Download substitutes vs build locally
- Differ if impure eval sneaks in (
builtins.currentSystemmisuse, unpinnedfetchTarball, IFD surprises)
- Differ across architectures
Lock discipline is necessary, not always sufficient, for bit-identical everything—but it is the main lever you control daily.
Theory 7 — Commit policy for locks
| Do | Don’t |
|---|---|
Commit flake.lock with the change that needs it |
Gitignore lock for NixOS host flakes |
| One logical update per commit when possible | Mix lock bumps with huge unrelated refactors |
| Note why you updated in commit body | “chore: update” with no smoke test |
Example commit body:
flake: update nixpkgs (26.05 tip)
Smoke: sshd active, rebuild switch OK, hello from devShell OK.
Worked example — Selective update narrative
Monday: update nixpkgs only → rebuild lab → OK
Tuesday: add home-manager input → lock appears → rebuild
Friday: update home-manager only → HM activation issue → revert lock hunk
Reverting a bad lock is often:
git checkout HEAD~1 -- flake.lock
sudo nixos-rebuild switch --flake .#labThat is faster than debugging three simultaneous input moves.
Lab 1 — Snapshot current pins
Suggested workspace: your Stage III flake.
cd /path/to/your-flake
nix flake metadata
cp flake.lock /tmp/flake.lock.day24.bak
git statusJournal:
- List every input name and its locked
rev(short hash is fine)
- Confirm
flake.lockis tracked by git
- Note whether any input uses
follows
Lab 2 — Read the lock structure
# human overview
nix flake metadata
# if jq available
nix flake metadata --json | jq '.locks.nodes | keys'Open flake.lock and identify:
- The
rootnode’s inputs
- The
nixpkgsnode’srevandnarHash
- Any nested
nixpkgs_2style duplicates (candidate forfollowslater)
Lab 3 — Selective update of one input
Only if you accept rebuild churn today. On a disposable lab host:
nix flake update nixpkgs
git diff --stat flake.lock
git diff flake.lock | head -120Build before switch:
nixos-rebuild build --flake .#lab
# if build OK:
sudo nixos-rebuild switch --flake .#labSmoke checklist:
systemctl is-system-running || true
systemctl is-active sshd
nix --versionIf break: restore backup lock and re-switch.
Lab 4 — Prove “update all” is louder
Do not leave the system on a broken all-update. On a throwaway branch:
git checkout -b experiment/flake-update-all
nix flake update
git diff --stat flake.lockCount how many inputs / revs moved. Then:
git checkout main # or your real branch name
git branch -D experiment/flake-update-all # if you discard
# ensure flake.lock restoredWrite one sentence: why selective update is the default for hosts.
Lab 5 — Document house rules
Create docs/lock-policy.md (or journal):
# Lock policy (lab)
- Track nixpkgs nixos-26.05
- Commit flake.lock always
- Update one input per change window
- Smoke: rebuild + SSH + one app path
- Revert lock on failure before deep debugOptional: add a shell alias or just muscle memory—not a requirement.
Lab 6 — Registry awareness (read-only)
nix registry listCompare:
nix flake metadataNote if the registry’s nixpkgs rev differs from the flake’s. No need to change registries today.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Everyone’s host differs after clone | Missing or uncommitted flake.lock |
follows not set → two nixpkgs |
Add inputs.foo.inputs.nixpkgs.follows = "nixpkgs" |
| Updated lock, forgot rebuild | switch / boot to activate |
CI uses nix flake update every run |
Pin lock in repo; update in deliberate PRs |
| “I’m on 26.05” but lock is months old | Expected until you update; schedule updates |
Restored flake.nix but not lock |
Always restore the pair when debugging inputs |
| Private input auth fails after update | Token/SSH access issue, not lock format |
Checkpoint
- Can explain what
rev+narHashbuy you
- Can update one named input and read the lock diff
- Have a written house lock policy
- Know how to revert
flake.lockquickly
- Understand registry ≠ flake pin
followsplanned or applied for future HM input
Commit
git add .
git commit -m "day24: lock discipline and update policy"Write three personal gotchas before continuing.
Tomorrow
Day 25 — Home Manager on NixOS: add Home Manager as a NixOS module so system and user activation share one rebuild story.
Day 25 — Home Manager on NixOS
Stage III · ~4h
Goal: Wire Home Manager as a NixOS module (not standalone-first), manage one user’s shell/git/ssh client config, and rebuild with a single nixos-rebuild switch --flake activation story.
House default for this volume: Home Manager NixOS module on NixOS hosts. Standalone HM is for non-NixOS machines or special cases—know it exists; do not make it your lab’s primary path.
Why this day exists
Stage II put packages on the system (environment.systemPackages). That is correct for daemons and multi-user tools—and wrong as the only place for personal developer config. Home Manager (HM) owns:
- User programs (
programs.git,programs.bash/zsh/fish, …)
- Dotfiles and XDG (Day 26)
- User systemd services (Day 27)
Doing HM as a NixOS module means: one flake, one rebuild, one generation story for system + user.
Theory 1 — Three ways people run Home Manager
| Mode | How it activates | When to use |
|---|---|---|
| NixOS module | Part of nixos-rebuild |
Default on NixOS (this book) |
| Standalone | home-manager switch --flake ... |
Other distros; or isolated user experiments |
| nix-darwin module | macOS analog | Out of scope here |
Why module mode first
- Aligns HM with system generations (easier mental model for labs)
users.users.<name>andhome-manager.users.<name>stay in one repo
- Avoids “system switched but HM forgotten” dual-track ops
Standalone still matters later if you develop on Ubuntu with HM-only—but your NixOS lab should not require two tools for every change.
Theory 2 — Input wiring (26.05-aligned)
# flake.nix (inputs section — merge with your layout)
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
home-manager.url = "github:nix-community/home-manager/release-26.05";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, home-manager, ... }: {
nixosConfigurations.lab = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./hosts/lab
home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
# home-manager.extraSpecialArgs = { inherit inputs; };
home-manager.users.alice = import ./homes/alice;
}
];
};
};
}| Option | Meaning |
|---|---|
useGlobalPkgs = true |
HM uses the same pkgs as the system (consistent overlays later) |
useUserPackages = true |
User packages install into user profile via HM |
home-manager.users.alice = ... |
That user’s HM module tree |
Version match: HM release-26.05 with nixpkgs nixos-26.05. Mismatched releases are a common source of obscure option errors.
Theory 3 — System user vs HM user
You still need a NixOS user:
# modules/common/users.nix
{
users.users.alice = {
isNormalUser = true;
extraGroups = [ "wheel" ];
# shell can be set here OR via HM programs.* — pick one source of truth
};
}HM configures that user’s home:
# homes/alice/default.nix
{ pkgs, ... }:
{
home.username = "alice";
home.homeDirectory = "/home/alice";
home.stateVersion = "26.05"; # set once; do not casually bump
programs.home-manager.enable = true;
programs.git = {
enable = true;
userName = "Alice Example";
userEmail = "alice@example.invalid";
};
programs.bash = {
enable = true;
enableCompletion = true;
};
home.packages = with pkgs; [
ripgrep
fd
];
}home.stateVersion
Like NixOS system.stateVersion, this is a compatibility pin for HM stateful defaults—not “current release marketing.” Set it to the HM release you first adopt for that user; read release notes before bumping.
Theory 4 — What belongs in HM vs NixOS
| Concern | Prefer |
|---|---|
sshd, firewall, kernel, system users |
NixOS modules |
| Interactive shell, prompt, git, editor config | HM |
| CLI tools only you use | HM home.packages |
| Libraries/services shared by system units | NixOS |
| Secrets for user apps | Stage IV patterns (not plaintext in HM) |
Anti-pattern: dumping everything into environment.systemPackages because HM felt optional. Opposite anti-pattern: putting nginx only in HM.
Theory 5 — Activation model (what rebuild does)
On nixos-rebuild switch with HM module:
- System config builds
- HM config for each defined user builds
- Activation scripts run: system switch + HM activation for users
Failures can be system or HM. Read the traceback: home-manager-alice.service style units often appear in logs.
journalctl -u home-manager-alice.service -b --no-pager | tail -80(Username substitution as appropriate.)
Theory 6 — Programs modules vs raw packages
HM programs.* modules often:
- Install the package
- Generate config files
- Wire shell integration
Example: programs.git.enable = true vs only home.packages = [ pkgs.git ] (no managed config). Prefer programs modules when they exist for tools you configure.
Discover options:
- Home Manager option search
- Or eval/docs from your locked HM input
Worked example — Minimal useful user
# homes/alice/default.nix
{ pkgs, lib, ... }:
{
home.username = "alice";
home.homeDirectory = "/home/alice";
home.stateVersion = "26.05";
programs.home-manager.enable = true;
programs.git = {
enable = true;
userName = "Alice";
userEmail = "alice@example.invalid";
extraConfig = {
init.defaultBranch = "main";
pull.rebase = true;
};
};
programs.ssh = {
enable = true;
# Do not put private keys in the flake. Config only:
matchBlocks = {
"lab-remote" = {
hostname = "192.0.2.10"; # documentation range example
user = "alice";
};
};
};
programs.bash = {
enable = true;
shellAliases = {
ll = "ls -la";
".." = "cd ..";
};
};
home.packages = with pkgs; [ htop ];
}Private keys stay in ~/.ssh/ with correct permissions—never in git, never as world-readable store strings (Stage IV deep dive).
Lab 1 — Add the input and lock
cd /path/to/your-flake
# edit flake.nix inputs + outputs as in Theory 2
nix flake lock
# or: nix flake update home-manager
git diff flake.lock | head -80Confirm follows for nixpkgs.
Lab 2 — Stub → real home module
mkdir -p homes/aliceWrite homes/alice/default.nix with git + bash + one package. Wire home-manager.nixosModules.home-manager and home-manager.users.<you>.
Ensure users.users.<you> exists on the system side with matching name and home directory.
Lab 3 — Rebuild and verify
sudo nixos-rebuild switch --flake .#labAs the target user:
git config --global --list | head
which rg || which htop
echo "$HOME"Check HM generation awareness (command names vary slightly by setup):
home-manager generations 2>/dev/null || true
ls -la ~/.local/state/nix/profiles 2>/dev/null || trueLab 4 — Change only HM, rebuild once
Edit programs.git.userName, rebuild, confirm new value. You should not need a separate home-manager switch if module mode is correct.
Lab 5 — Deliberate failure drill
Introduce a typo option:
programs.git.enabl = true; # intentional typoRebuild, read the error, fix. Note whether the failure is clearly HM evaluation.
Lab 6 — Journal: boundary decisions
List 10 tools you use daily. Mark each NixOS / HM / either. Resolve at least the ambiguous ones with a one-line reason.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| HM release ≠ nixpkgs release | Align release-26.05 with nixos-26.05 |
| User name mismatch | users.users.x must match home-manager.users.x |
home.homeDirectory wrong |
Must match actual home path |
Infinite recursion with useGlobalPkgs |
Check for pkgs/HM cycles; simplify modules |
| Expecting standalone CLI only | Module mode activates via nixos-rebuild |
| Private keys in flake | Remove immediately; rotate if committed |
Bumping home.stateVersion casually |
Read HM release notes first |
| Shell set in three places | One source of truth (NixOS users.users.shell or HM) |
Checkpoint
- HM input pinned with
followsnixpkgs
- HM NixOS module imported in flake
- At least one user has git + shell via HM
- Single
nixos-rebuild switch --flakeactivates system + HM
- No private key material in the repo
home.stateVersionset consciously
Commit
git add .
git commit -m "day25: home-manager as NixOS module"Write three personal gotchas before continuing.
Tomorrow
Day 26 — HM files & XDG: home.file, xdg.configFile, permissions, and migrating real dotfiles without turning the store into a secret dump.