Day 11 — flake-parts, Checks, Overlays & Gate II

Updated

July 30, 2025

Day 11 — flake-parts, Checks, Overlays & Gate II

Day 29 — flake-parts or discipline

Stage III · ~4h
Goal: Choose a house style for multi-output flakes—either adopt flake-parts to reduce boilerplate, or enforce a hand-rolled structure with equal consistency—and apply it to your Stage III flake.

Note

There is no moral victory in either choice. There is only consistency. Mixing half flake-parts and half ad-hoc outputs = args: … is the failure mode.

Why this day exists

By now your flake may export:

  • nixosConfigurations.*
  • devShells.* (host-level tooling)
  • Soon: checks.*, maybe packages.*, formatter

Raw outputs functions grow nested let blocks and copy-pasted system lists. Two responses exist in the ecosystem:

Style Idea
flake-parts Framework: modules for flakes; per-system helper; community modules
Hand-rolled discipline Small local helpers (forAllSystems), strict file layout, no new dependency

Today you pick one and document it.


Theory 1 — Pain that motivates structure

Typical messy outputs:

outputs = { self, nixpkgs, home-manager, ... }@inputs:
let
  system = "x86_64-linux";
  pkgs = import nixpkgs { inherit system; };
in {
  nixosConfigurations.lab = /* ... */;
  devShells.${system}.default = /* ... */;
  # forgot aarch64; forgot checks; formatter missing
};

Problems:

  • Single-system assumptions
  • Repeated pkgs construction
  • Hard to share patterns across repos
  • Easy to forget an output class

Theory 2 — Hand-rolled discipline (no flake-parts)

Pattern used widely:

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
  # ...

  outputs = { self, nixpkgs, ... }@inputs:
  let
    systems = [ "x86_64-linux" "aarch64-linux" ];
    forAllSystems = nixpkgs.lib.genAttrs systems;
    pkgsFor = system: import nixpkgs {
      inherit system;
      # overlays = [ self.overlays.default ];
    };
  in {
    nixosConfigurations.lab = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      specialArgs = { inherit inputs; };
      modules = [ ./hosts/lab /* hm module wiring */ ];
    };

    devShells = forAllSystems (system:
      let pkgs = pkgsFor system; in {
        default = pkgs.mkShell {
          packages = with pkgs; [ nixpkgs-fmt nil ];
        };
      });

    formatter = forAllSystems (system: (pkgsFor system).nixpkgs-fmt);
  };
}

House rules for hand-rolled

  1. One systems list
  2. One pkgsFor / overlay injection point
  3. Host modules stay under hosts/; flake stays wiring
  4. New output types get a comment in docs/flake-style.md
  5. No deep logic in flake.nix—call into ./lib or ./flake/ if needed

Theory 3 — flake-parts approach

flake-parts turns outputs into a module system for flakes.

{
  description = "lab with flake-parts";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
    flake-parts.url = "github:hercules-ci/flake-parts";
    home-manager.url = "github:nix-community/home-manager/release-26.05";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = inputs@{ flake-parts, ... }:
    flake-parts.lib.mkFlake { inherit inputs; } {
      systems = [ "x86_64-linux" "aarch64-linux" ];

      perSystem = { pkgs, system, ... }: {
        devShells.default = pkgs.mkShell {
          packages = with pkgs; [ nixpkgs-fmt nil ];
        };
        formatter = pkgs.nixpkgs-fmt;
      };

      flake = {
        nixosConfigurations.lab = inputs.nixpkgs.lib.nixosSystem {
          system = "x86_64-linux";
          specialArgs = { inherit inputs; };
          modules = [
            ./hosts/lab
            inputs.home-manager.nixosModules.home-manager
            {
              home-manager.useGlobalPkgs = true;
              home-manager.useUserPackages = true;
              home-manager.users.alice = import ./homes/alice;
            }
          ];
        };
      };
    };
}
Concept Role
perSystem Outputs that vary by system (packages, devShells, checks, formatter)
flake = { ... } Top-level flake attrs like nixosConfigurations
Modules You can split flake-parts config across files

Costs

  • Another input to pin and learn
  • Stack traces include framework layers
  • Overkill for a single nixosConfigurations only—but you are past that

Theory 4 — Decision rubric

Choose flake-parts if… Choose hand-rolled if…
You expect many per-system outputs You want minimal dependencies
You like module-shaped config everywhere You already understand raw flakes well
You will copy patterns across many repos One lab flake is enough for months
You accept framework upgrades You prefer total local control

Book stance: either is valid. Capstone graders care that you have a style, not which logo you use.


Theory 5 — What not to do

  • Rewrite layout (Day 23) and adopt flake-parts in the same panicked hour without rebuild
  • Import flake-parts and still paste a second full outputs tree beside it
  • Put NixOS module logic inside perSystem incorrectly (hosts are usually under flake.nixosConfigurations)
  • Enable every community flake-parts module on day one

Theory 6 — Formatter as a style signal

Regardless of framework, expose a formatter:

nix fmt

With formatter output set, this becomes a team habit before Day 30’s checks.


Worked example — Split hand-rolled helpers

# lib/flake-utils.nix
{ nixpkgs }:
rec {
  systems = [ "x86_64-linux" "aarch64-linux" ];
  forAllSystems = nixpkgs.lib.genAttrs systems;
  pkgsFor = system: import nixpkgs { inherit system; };
}
# flake.nix uses:
# let u = import ./lib/flake-utils.nix { inherit nixpkgs; }; in …

Keeps flake.nix readable without flake-parts.


Lab 1 — Audit current outputs

cd /path/to/your-flake
nix flake show

Journal:

  1. Which outputs exist today?
  2. Which will exist by Day 32 (checks, shells, …)?
  3. Is flake.nix already painful?

Lab 2 — Pick and write the decision

Create docs/flake-style.md:

# Flake style

Decision: hand-rolled | flake-parts
Date: YYYY-MM-DD
Rationale: …

Rules:
- 

Commit this even before code changes—forces intentionality.


Lab 3A — If flake-parts: adopt on a branch

git checkout -b experiment/flake-parts
# add input, rewrite outputs via mkFlake
nix flake lock
nix flake show
nixos-rebuild build --flake .#lab
sudo nixos-rebuild switch --flake .#lab   # if build OK

Prove HM + host still activate.


Lab 3B — If hand-rolled: extract helpers

git checkout -b experiment/flake-discipline
# add systems + forAllSystems + formatter + devShells
nix flake show
nix fmt 2>/dev/null || true
nix develop -c true
nixos-rebuild build --flake .#lab

Lab 4 — Merge the winner

Merge the experiment branch to your main lab branch. Delete half-finished alternatives. One style in flake.nix.


Lab 5 — Teach the style to “future you”

In docs/flake-style.md, document how to:

  1. Add a new nixosConfigurations host
  2. Add a per-system devShell
  3. Update flake-parts (if used) or helpers (if not)

Common gotchas

Symptom / mistake What to do
perSystem vs flake confusion Hosts go under flake; shells under perSystem
Lost specialArgs / inputs Thread inputs explicitly after rewrite
HM module dropped in refactor Re-check modules list post-migration
Double nixpkgs without follows Keep follows on all companion inputs
nix flake show empty-ish Eval error—run build to see full trace
Framework without docs Write house docs the same day

Checkpoint

  • Explicit house style documented
  • nix flake show reflects multi-output intent
  • Host rebuild still works
  • formatter and/or devShells exist in the chosen style
  • No hybrid mess left on main branch
  • How-to for adding a host written

Commit

git add .
git commit -m "day29: flake-parts or hand-rolled discipline"

Write three personal gotchas before continuing.


Tomorrow

Day 30 — Flake checks: make nix flake check fail when format/eval assumptions break—CI-shaped discipline before packaging stage.


Day 30 — Flake checks

Stage III · ~4h
Goal: Add flake checks so nix flake check exercises eval/format/build assumptions locally—the same shape CI will use later.

Why this day exists

A flake that only switches on your laptop is a private ritual. Checks turn “I hope this still evaluates” into a command:

nix flake check

Today is not full nixosTest mastery (Stage VI). Today is lightweight gates: format, build a shell, evaluate a host, maybe build a package.


Theory 1 — What checks are

Flake schema: checks.<system>.<name> → derivations. nix flake check builds them (with some special cases).

Check type Purpose
Format / lint Style consistency
nixosSystem eval/build Config still evaluates
mkShell build DevShell inputs resolve
Small package build Packaging sanity
nixosTest VM integration (later)

Failed check → non-zero exit → CI red.


Theory 2 — Hand-rolled checks skeleton

# inside outputs, with forAllSystems / pkgsFor from Day 29
checks = forAllSystems (system:
  let
    pkgs = pkgsFor system;
  in {
    # 1) formatter dry-run style check
    format = pkgs.runCommand "check-format" {
      buildInputs = [ pkgs.nixpkgs-fmt ];
    } ''
      set -euo pipefail
      # copy sources; fail if nixpkgs-fmt wants changes
      mkdir -p $out
      # For a real check, point at your nix files via path builtins carefully.
      # Simpler starter: succeed if formatter exists
      command -v nixpkgs-fmt > $out/fmt-path
    '';

    # 2) devShell builds
    devshell = self.devShells.${system}.default;

    # 3) host evaluation via build of system toplevel (heavy but strong)
    # lab-host = self.nixosConfigurations.lab.config.system.build.toplevel;
  });

Building the full toplevel is an excellent check but can be slow and architecture-bound. For a laptop-only x86_64-linux lab, pinning checks to that system is fine.

Lighter host check — eval only patterns

Some people use pkgs.writeText of a JSON dump of options they care about, or nixos-rebuild build in CI scripts without putting toplevel in checks. Either works if documented.


Theory 3 — flake-parts checks

Under perSystem:

perSystem = { self', pkgs, ... }: {
  checks = {
    devshell = self'.devShells.default;
    # treefmt / pre-commit style modules exist in the ecosystem—optional
  };
};

Community modules (treefmt-nix, git-hooks.nix / pre-commit-hooks) are optional accelerators—do not require them today if time is short.


Theory 4 — Format check that actually fails

A useful format check:

format = pkgs.runCommand "check-nix-fmt" {
  nativeBuildInputs = [ pkgs.nixpkgs-fmt ];
  src = ./.;  # careful: large contexts; often better to pass explicit file list
} ''
  set -euo pipefail
  cp -r $src tree
  chmod -R u+w tree
  cd tree
  # list your nix files
  find . -name '*.nix' -print0 | xargs -0 nixpkgs-fmt --check
  mkdir -p $out
  touch $out/ok
'';

Notes:

  • --check must exist for your formatter CLI (nixpkgs-fmt supports check mode; nix fmt workflows vary)
  • Copying entire flake including .git can be heavy—trim with lib.cleanSource
src = lib.cleanSource ./.;

Theory 5 — What belongs in check vs push-time CI only

Local nix flake check CI later (Stage VI)
Format Same + matrix systems
Shell / small builds Cache push
Optional host toplevel Full VM tests

Keep local checks fast enough that you run them. A 40-minute check nobody runs is theater.


Theory 6 — nix flake check UX

nix flake check
nix flake check -L          # logs
nix build .#checks.x86_64-linux.format -L

Individual check attrs help debug without running the full set.


Worked example — Practical minimal set for Stage III

checks = forAllSystems (system:
  let pkgs = pkgsFor system; in {
    shell = self.devShells.${system}.default;
    # only on the host architecture you actually use:
  }) // {
  # top-level non-per-system alternative patterns exist; prefer per-system consistency
};

Plus on x86_64-linux only:

# conceptual
lab-toplevel = self.nixosConfigurations.lab.config.system.build.toplevel;

If aarch64 eval of an x86-only host is nonsense, do not genAttrs that check onto all systems blindly—gate by system.

checks = forAllSystems (system:
  let pkgs = pkgsFor system; in {
    shell = self.devShells.${system}.default;
  } // nixpkgs.lib.optionalAttrs (system == "x86_64-linux") {
    lab-toplevel = self.nixosConfigurations.lab.config.system.build.toplevel;
  });

Lab 1 — Baseline without checks

cd /path/to/your-flake
nix flake show
nix flake check || true

Note current behavior (may pass with zero checks or warn).


Lab 2 — Add devShell as a check

Wire checks.<system>.shell = self.devShells.${system}.default (or flake-parts equivalent).

nix build .#checks.x86_64-linux.shell -L

Lab 3 — Add a format check that can fail

  1. Implement format check on *.nix
  2. Run nix flake check
  3. Break formatting in a temp edit
  4. Confirm check fails
  5. Restore format
# example break
# add ugly spacing in a nix file, then:
nix flake check

Lab 4 — Optional host toplevel check

If disk/time allow:

nix build .#checks.x86_64-linux.lab-toplevel -L

Record wall-clock time. If > your patience budget, document “CI-only toplevel” and keep shell+format local.


Lab 5 — Developer loop script (optional)

# tools/check.sh
#!/usr/bin/env bash
set -euo pipefail
nix flake check -L

Not required if you prefer raw commands.


Lab 6 — Journal: CI shape

Write the future CI job in five lines:

checkout
install nix
nix flake check
# later: deploy job separate

Common gotchas

Symptom / mistake What to do
Checks empty / not discovered Must be under checks output correctly
IFD surprises slow checks Avoid unnecessary import-from-derivation
Building host on wrong system optionalAttrs gate
Format check always succeeds You didn’t pass real sources
Check context includes secrets Don’t; keep secrets out of flake source
nix flake check downloads the world First run cold; then cached—note substituters

Checkpoint

  • At least two meaningful checks (e.g. shell + format)
  • nix flake check fails when format intentionally broken
  • Know how to build one check attribute
  • Host toplevel check added or explicitly deferred with reason
  • Future CI sketch written
  • Checks are fast enough to run before commit

Commit

git add .
git commit -m "day30: flake checks for format and shell"

Write three personal gotchas before continuing.


Tomorrow

Day 31 — Overlays light: pin or patch one package for host and shell without forking all of nixpkgs.


Day 31 — Overlays light

Stage III · ~4h
Goal: Apply a small overlay so the same package change is visible to the NixOS host and to devShells—without forking nixpkgs or cargo-culting overlay stacks.

Important

Overlays are sharp tools. Prefer options, package overrides in a single module, or upstream pins when they suffice. Today teaches the mechanism so you are not helpless—not so every problem becomes an overlay.

Why this day exists

Sooner or later you need:

  • A newer/older minor of one tool than nixpkgs 26.05 ships
  • A one-line patch to a package you use
  • A renamed or wrapped package for your lab

Overlays let you modify the pkgs fixed-point. Done lightly, they are elegant. Done heavily, they make upgrades impossible.


Theory 1 — What an overlay is

An overlay is a function:

final: prev: {
  # new or overridden attrs
}
Argument Meaning
prev Package set before this overlay
final Package set after all overlays (fixed-point)—use for deps on other overridden pkgs
final: prev: {
  hello = prev.hello.overrideAttrs (old: {
    # example: add a patch or change configure flags
  });
}

Theory 2 — Wiring overlays on NixOS

# modules/common/nixpkgs.nix
{ ... }:
{
  nixpkgs.overlays = [
    (import ../../overlays/hello.nix)
    # or inline:
    # (final: prev: { ... })
  ];
}

With home-manager.useGlobalPkgs = true, HM sees the same overlayed pkgs. That is why Day 25 set that flag.

Flake-level pkgs for shells/checks

pkgsFor = system: import nixpkgs {
  inherit system;
  overlays = [
    (import ./overlays/hello.nix)
  ];
};

Consistency rule: the overlay list used for nixosConfigurations should match the list used for devShells / checks when you care about the same tools—or document intentional differences.


Theory 3 — overrideAttrs vs override vs new package

Tool When
pkg.override { ... } Change callPackage parameters
pkg.overrideAttrs Change derivation attributes (patches, version, src)
New attr final.mytool = ... Add a package name without clobbering

Example patch (illustrative):

# overlays/hello.nix
final: prev: {
  hello = prev.hello.overrideAttrs (old: {
    patches = (old.patches or []) ++ [
      # ./patches/hello-example.patch
    ];
  });
}

If you do not have a real patch today, use a harmless visible change such as pname suffix only for learning—or prefer adding a wrapper package instead of mutating hello:

final: prev: {
  hello-lab = prev.writeShellScriptBin "hello-lab" ''
    echo "lab overlay wrapper"
    exec ${prev.hello}/bin/hello "$@"
  '';
}

Wrappers are often safer than overriding core packages.


Theory 4 — Composition order

nixpkgs.overlays = [ overlayA overlayB ];

Later overlays see earlier results via prev chain. Conflicts: last writer wins for the same attr. Keep one file per concern and a short list.


Theory 5 — When not to overlay

Need Prefer
Enable a service option NixOS module option
Package only for one project Project devShell / packages
Temporary try nix shell / branch pin
Huge fork of desktop stack Separate nixpkgs input (advanced)

Overlays that rewrite half of python3Packages are Stage V+ territory—and often a mistake.


Theory 6 — Pinning a package from another nixpkgs (light)

Sometimes you want one package from a newer pin without moving the whole OS:

# advanced sketch — use sparingly
# inputs.nixpkgs-unstable.url = "...";
# final: prev: {
#   fancy = inputs.nixpkgs-unstable.legacyPackages.${prev.system}.fancy;
# }

Costs: two package graphs, closure size, ABI surprises. For today, a wrapper or small overrideAttrs is enough.


Worked example — Shared overlay module

overlays/
  default.nix      # composes list
  hello-lab.nix
modules/common/nixpkgs.nix
# overlays/default.nix
[
  (import ./hello-lab.nix)
]
# overlays/hello-lab.nix
final: prev: {
  hello-lab = prev.writeShellScriptBin "hello-lab" ''
    echo "hello from Stage III overlay"
    exec ${prev.hello}/bin/hello "$@"
  '';
}
# modules/common/nixpkgs.nix
{ ... }:
{
  nixpkgs.overlays = import ../../overlays/default.nix;
  environment.systemPackages = [
    # pkgs.hello-lab  # add via a small packages module once pkgs sees overlay
  ];
}

In a module that receives pkgs:

{ pkgs, ... }:
{
  environment.systemPackages = [ pkgs.hello-lab ];
}

And in flake pkgsFor, import the same overlay list.


Lab 1 — Create overlay file

mkdir -p overlays
# add hello-lab.nix and default.nix as above

Lab 2 — Attach to NixOS

Import overlay list via nixpkgs.overlays. Add pkgs.hello-lab to system or HM packages. Rebuild:

sudo nixos-rebuild switch --flake .#lab
hello-lab

Lab 3 — Attach to devShell / checks pkgs

Ensure pkgsFor (or flake-parts pkgs) uses the same overlays.

nix develop -c hello-lab

If only system has it, fix the flake pkgs construction.


Lab 4 — Prove HM sees it (if useGlobalPkgs)

# homes/alice snippet
home.packages = [ pkgs.hello-lab ];

Rebuild; as user command -v hello-lab.


Lab 5 — Intentional override discipline

Journal:

  1. What you overlaid/wrapped and why
  2. How you will remove it when upstream catches up
  3. Whether a project-only shell would have been enough

Lab 6 — Negative space

Find one change you could do with an overlay but will do with a module option instead. Write it down—builds judgment.


Common gotchas

Symptom / mistake What to do
pkgs.hello-lab missing Overlay not wired into that pkgs import
HM doesn’t see overlay useGlobalPkgs false or separate pkgs
Infinite recursion in overlay Using final incorrectly; simplify
Overlay only on system Wire flake pkgsFor too
Overlaying all of nixpkgs for fun Revert; minimize surface
Patch path wrong Path relative to overlay file / flake root

Checkpoint

  • Overlay or wrapper package defined in repo
  • Visible on NixOS host packages
  • Visible in devShell or documented intentional difference
  • HM consistency verified if applicable
  • Removal plan written
  • Judgment note: when not to overlay

Commit

git add .
git commit -m "day31: light overlays shared by host and shell"

Write three personal gotchas before continuing.


Tomorrow

Day 32 — Stage III gate: prove host + Home Manager + devShell + checks live in one clean flake—exit criteria for daily-driver shape.


Day 32 — Stage III gate

Stage III · ~4h
Goal: Prove the Stage III exit criteria: a workstation/server skeleton where host + Home Manager + project/devShell workflow + flake checks live in one coherent flake layout—and a new machine of the same role is “clone + hardware + rebuild.”

Important

Gates are not content cram days. They are proof. If something is missing, fix it here before Stage IV secrets and services multiply the mess.

Why this day exists

Stage III introduced structure without which Stage IV becomes dangerous:

Stage III skill Stage IV needs it because…
hosts/modules/homes Secrets modules attach cleanly
Lock discipline New inputs (sops-nix) must pin sanely
HM as NixOS module User + system activate together
direnv/devShell Service dev doesn’t bloat system
checks Catch eval breakage before deploy
light overlays Occasional pin without chaos

Today you demonstrate, not skim blog posts.


Theory 1 — Official exit criteria (from syllabus)

New machine of same role = clone flake + hardware config + rebuild.

Operationalization:

  1. Flake is the source of truth (no critical imperative snowflakes)
  2. Layout is navigable by someone else (or future you)
  3. User environment is HM-managed for shell/git basics
  4. Project tools are not only global profiles
  5. nix flake check (your subset) passes
  6. Rebuild from cold boot still works; rollback still works

Theory 2 — Evidence pack (what “done” looks like)

Prepare a short evidence directory (journal or docs/stage-iii-evidence.md):

# Stage III evidence

## Tree
(paste tree -L 3)

## Commands
- nix flake show
- nix flake check
- nixos-rebuild switch --flake .#lab
- direnv / nix develop proof
- home-manager user programs proof

## Pins
- nixpkgs rev: …
- home-manager rev: …

## Known debt
- 

Theory 3 — Role skeleton checklist

Host (hosts/lab)

  • Hostname set
  • Hardware imported
  • Common modules imported
  • SSH keys-only access still true
  • Firewall stance known (even if simple)
  • system.stateVersion set

Modules

  • Common policy split (users/ssh/nix/…)
  • At least one custom module still present from Stage II
  • Overlay wiring intentional

Homes

  • HM NixOS module wired
  • User programs: shell + git
  • At least one managed file or XDG config
  • Optional user service still present

Flake outputs

  • nixosConfigurations.<host>
  • devShells or project-flake workflow documented
  • checks nonempty and passing
  • formatter optional but nice
  • Style doc (flake-parts vs hand-rolled)

Theory 4 — Clone drill (mental or real)

Ideal proof on a second VM:

git clone <your-flake-url> lab && cd lab
# generate or copy hardware-configuration for THIS machine
sudo nixos-rebuild switch --flake .#lab

If you lack a second VM today: walk the steps dry and list what would differ (disk UUIDs, hostname, network).


Theory 5 — Debt register

Unfinished work is allowed only if listed:

Debt Stage to pay
Secrets still plaintext IV (33–35) urgently
No format check Fix today if <1h
Theming rabbit hole Never required
Multi-host Later stages

Do not enter Stage IV with “I will structure the flake later.”


Lab 1 — Tree and show

cd /path/to/your-flake
find . -type f \( -name '*.nix' -o -name '*.md' -o -name '.envrc' \) | sort
nix flake metadata
nix flake show

Paste into evidence doc.


Lab 2 — Full rebuild proof

sudo nixos-rebuild switch --flake .#lab

Verify:

hostname
systemctl is-active sshd
git config --global --get user.name || true
command -v direnv

Lab 3 — Checks pass

nix flake check -L

Fix failures before continuing. If toplevel check is too heavy, ensure documented lighter checks still pass.


Lab 4 — DevShell / direnv proof

cd ~/lab/90daysofx/02-nixos/day28/demo-app  # or your project
# show tools available via direnv or nix develop
nix develop -c true

Lab 5 — Rollback still works (confidence)

# list generations
sudo nix-env -p /nix/var/nix/profiles/system --list-generations | tail

You do not need to break the system today if already proven in Stage II—but confirm you still know the boot menu / switch --rollback path. Write the commands in evidence.


Lab 6 — Peer-style review of your own flake

Answer in writing:

  1. Where do I add a second host?
  2. Where do I add a system service module?
  3. Where do I add a user package?
  4. How do I update only nixpkgs?
  5. What is still imperative on this machine?

If any answer is “I don’t know,” fix docs or layout now.


Lab 7 — Gate commit and tag (optional)

git add .
git commit -m "day32: stage III gate — host+HM+shell+checks"
git tag -a stage-iii-gate -m "Stage III exit"

Tags help Stage VIII capstone archaeology.



Theory 6 — Stage III → IV interface contract

Stage IV will attach:

Concern Expected hook in your flake
sops-nix / agenix module import + secrets files outside store plaintext
reverse proxy modules/services/ style
data service state dirs + secrets paths
firewall matrix docs/ports.md living doc

If your layout has nowhere obvious to put those, redesign today.


Theory 7 — Pin coherence check

nix flake metadata
# home-manager should follow nixpkgs when you set follows

Mismatched HM release vs nixpkgs (e.g. HM 25.11 on nixpkgs 26.05) is a classic eval footgun. Align release-26.05 tracks.

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";
};

Worked example — Minimal checks still count

checks.${system}.hostname = pkgs.runCommand "check-hostname" { } ''
  # example pattern only — prefer real eval checks you already have
  echo ok > $out
'';

Better: evaluate that nixosConfigurations.lab exists and maybe that config.networking.hostName matches expectation (as you did in Day 30). Gate requires passing checks, not toy checks that ignore the host.


Lab 8 — HM vs system package boundary audit

List five tools and where they live:

Tool systemPackages HM home.packages devShell
git ? ? ?

Prefer: system or HM for daily drivers; devShell for language toolchains.


Lab 9 — flake.lock age honesty

git log -1 --format=%ci -- flake.lock

If the lock is months stale on a public-facing host, schedule an update session—not a blind full update on this gate day unless you have time to smoke-test.

Common gotchas

Symptom / mistake What to do
Gate day used to learn HM from zero Return to Day 25; don’t fake the gate
Checks failing “but switch works” Fix checks—CI will not care about switch on your laptop
Evidence only in head Write the evidence file
Second host impossible today Dry-run clone steps still required
Secret debt ignored Escalate to Day 33 first thing next stage
Layout relapsed into monolith Re-split before Stage IV

Checkpoint — Stage III exit

  • Layout: hosts / modules / homes
  • Lock policy exists; lock committed
  • HM as NixOS module; shell + git managed
  • Files/XDG or programs configs real
  • direnv/devShell workflow works
  • Flake style chosen and documented
  • nix flake check passes
  • Light overlay optional but understood
  • Evidence doc written
  • Clone + hardware + rebuild story clear

Commit

git add .
git commit -m "day32: stage III gate"

Write three personal gotchas before continuing.


Tomorrow

Day 33 — Secret problem: why strings in Nix are dangerous, how the store leaks “secrets,” and what patterns Stage IV will replace.