Day 25 — Home Manager on NixOS

Updated

July 30, 2026

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.

Note

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> and home-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:

  1. System config builds
  2. HM config for each defined user builds
  3. 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:


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 -80

Confirm follows for nixpkgs.


Lab 2 — Stub → real home module

mkdir -p homes/alice

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

As 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 || true

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

Rebuild, 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 follows nixpkgs
  • HM NixOS module imported in flake
  • At least one user has git + shell via HM
  • Single nixos-rebuild switch --flake activates system + HM
  • No private key material in the repo
  • home.stateVersion set 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.