Home Manager on NixOS

Updated

July 30, 2026

Home Manager on NixOS

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 book: 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 chapter exists

Host modules 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 (next chapter)
  • User systemd services (HM services chapter)

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 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 (merge with your layout)
{
  description = "NixOS lab with Home Manager";

  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, ... }@inputs: {
    nixosConfigurations.lab = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      specialArgs = { inherit inputs; };
      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
extraSpecialArgs Pass flake inputs into HM modules when needed

Version match: HM release-26.05 with nixpkgs nixos-26.05. Mismatched releases are a common source of obscure option errors.

Where to put the HM block

Style Pros
Inline in flake.nix as above Obvious for one host
Dedicated module modules/common/home-manager.nix Cleaner multi-host
Per-host in hosts/lab/default.nix Different users per machine

All are valid; pick one and document it.


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.

Name and path must match

Field Must agree with
home-manager.users.alice attr name
home.username "alice"
home.homeDirectory actual home, usually /home/alice
users.users.alice system user exists

Mismatches produce activation failures or configs landing in the wrong place.


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 Secrets chapters (not plaintext in HM)
Network daemons (nginx, postgres) NixOS
Project compilers Project devShells + direnv (later chapter)

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.)

Standalone commands (awareness)

Even in module mode you may see:

home-manager generations 2>/dev/null || true
ls -la ~/.local/state/nix/profiles 2>/dev/null || true

You should not need home-manager switch for normal changes if module mode is correct.


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:

Shell source of truth

Approach Note
users.users.alice.shell = pkgs.bashInteractive System login shell
programs.bash.enable = true HM manages bash config
Both fighting Pick login shell once; let HM own rc files

Theory 7 — Splitting the home tree

homes/alice/
  default.nix      # imports + identity (username, stateVersion)
  programs.nix     # git, bash, ssh client
  files.nix        # later: xdg / home.file
  services.nix     # later: user systemd
# homes/alice/default.nix
{
  imports = [
    ./programs.nix
    # ./files.nix
    # ./services.nix
  ];

  home.username = "alice";
  home.homeDirectory = "/home/alice";
  home.stateVersion = "26.05";
  programs.home-manager.enable = true;
}

Theory 8 — Backup and collision with existing dots

HM activation may refuse to overwrite an existing regular file with a symlink. Before first activation of a path:

ls -la ~/.bashrc ~/.config/git 2>/dev/null || true
# backup and move aside if needed
Situation Approach
Imperative bashrc Backup; let HM own it
Mixed partial HM Finish migration; avoid dual management
One-off experiment file Keep outside HM paths

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 (secrets chapters).


Worked example — HM wiring module (multi-host friendly)

# modules/common/home-manager.nix
{ inputs, ... }:
{
  imports = [
    inputs.home-manager.nixosModules.home-manager
  ];

  home-manager = {
    useGlobalPkgs = true;
    useUserPackages = true;
    users.alice = import ../../homes/alice;
  };
}

Requires specialArgs = { inherit inputs; }; on nixosSystem (or equivalent) so inputs is available.


Exercises

Exercise 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.

Exercise 2 — Stub → real home module

mkdir -p homes/alice

Write homes/alice/default.nix with git + bash + one package. Wire HM NixOS module and home-manager.users.<you>.

Ensure users.users.<you> exists on the system side with matching name and home directory.

Exercise 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"
home-manager generations 2>/dev/null || true

Exercise 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.

Exercise 5 — Deliberate failure drill

programs.git.enabl = true; # intentional typo

Rebuild, read the error, fix. Note whether the failure is clearly HM evaluation.

Exercise 6 — Journal: boundary decisions

List 10 tools you use daily. Mark each NixOS / HM / either / project shell. Resolve at least the ambiguous ones with a one-line reason.

Exercise 7 — Activation log literacy

journalctl -u home-manager-$USER.service -b --no-pager | tail -100

Save one successful and (from Exercise 5) one failed snippet in notes (redact secrets).

Exercise 8 — Split programs.nix

Move git/bash/ssh into homes/alice/programs.nix and import it. Rebuild; behavior unchanged.

Exercise 9 — Package placement check

Add a tool to home.packages, rebuild, verify command -v. Confirm it is not required for other users of the host.

Exercise 10 — Shell source of truth

Document in docs/layout.md (or home notes): where login shell is set, where aliases live.

Exercise 11 — Backup existing dots

Before enabling a programs.* that manages a file you already have, backup:

cp -a ~/.gitconfig /tmp/gitconfig.bak 2>/dev/null || true

Exercise 12 — Version alignment proof

nix flake metadata | tee /tmp/flake-meta.txt

Confirm HM input tracks release-26.05 and nixpkgs nixos-26.05.

Exercise 13 — Negative case: standalone not required

Write two sentences: when you would use standalone HM; why the lab does not.


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 or HM for login shell)
Activation blocked by existing file Backup/remove blocker
Forgot follows Two nixpkgs; fix and re-lock
HM user without system user Create users.users.* first
Putting sshd in HM System service → NixOS module

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
  • Boundary list (NixOS vs HM vs project) started
  • Know where to read HM activation failures

Journal (optional)

Write three personal gotchas before continuing.