4 11 Flakes
Nimmo edited this page 2026-05-30 22:40:08 +01:00

Chapter 11: Flakes

Flakes are Nix's system for making projects self-contained and reproducible. A flake is defined by two files: flake.nix (what you write) and flake.lock (what Nix generates to pin versions).

Why Flakes Exist

Before flakes, NixOS configurations had two problems:

  1. No pinning. When you used <nixpkgs>, it referred to whatever version of nixpkgs was on your system's channel. Two people with the same configuration.nix could get different systems depending on when they last ran nix-channel --update.

  2. No standard structure. There was no agreement on how a Nix project should declare its dependencies or expose its outputs.

Flakes solve both problems. They pin every dependency to an exact revision and provide a standard schema for inputs and outputs.

Anatomy of flake.nix

Your flake.nix has two main sections: inputs and outputs.

inputs

This declares the external dependencies your project needs. Each input has a URL that tells Nix where to fetch it.

inputs = {
  nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
  nixpkgs-unstable-small.url = "github:NixOS/nixpkgs/nixos-unstable-small";
  nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-25.11";

  home-manager.url = "github:nix-community/home-manager";
  home-manager.inputs.nixpkgs.follows = "nixpkgs";

  claude-code-nix.url = "github:sadjow/claude-code-nix";
  claude-code-nix.inputs.nixpkgs.follows = "nixpkgs";

  codex-cli-nix.url = "github:sadjow/codex-cli-nix";
  codex-cli-nix.inputs.nixpkgs.follows = "nixpkgs";

  nixos-hardware.url = "github:NixOS/nixos-hardware/master";

  disko.url = "github:nix-community/disko";
  disko.inputs.nixpkgs.follows = "nixpkgs";

  bash-it.url = "github:Bash-it/bash-it";
  bash-it.flake = false;  # Not a flake, just a source repository

  nur.url = "github:nix-community/NUR";
  nur.inputs.nixpkgs.follows = "nixpkgs";

  sops-nix.url = "github:Mic92/sops-nix";
  sops-nix.inputs.nixpkgs.follows = "nixpkgs";

  plasma-manager.url = "github:nix-community/plasma-manager";
  plasma-manager.inputs.nixpkgs.follows = "nixpkgs";
  plasma-manager.inputs.home-manager.follows = "home-manager";

  framework-system.url = "github:FrameworkComputer/framework-system";
  framework-system.inputs.nixpkgs.follows = "nixpkgs";
};

Input URL formats

# GitHub repository, specific branch
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
#               ^owner  ^repo       ^branch

# GitHub repository, default branch
claude-code-nix.url = "github:sadjow/claude-code-nix";

follows

claude-code-nix.inputs.nixpkgs.follows = "nixpkgs";

The claude-code-nix flake has its own nixpkgs input. Without follows, it would use a separate version of nixpkgs — wasteful and potentially incompatible. follows = "nixpkgs" says: "When claude-code-nix asks for its nixpkgs, give it our nixpkgs instead." This ensures everything builds against the same package set. Use follows for any input that itself depends on nixpkgs.

Your inputs explained

Input Purpose Channel
nixpkgs The main NixOS package repository unstable (bleeding edge)
nixpkgs-unstable-small Smaller, faster-moving unstable subset electra
nixpkgs-stable Stable package import escape hatch 25.11 release
home-manager User environment management follows nixpkgs
claude-code-nix Claude Code CLI follows nixpkgs
codex-cli-nix Codex CLI follows nixpkgs
nixos-hardware Hardware-specific optimizations master
disko Declarative disk partitioning follows nixpkgs
bash-it Bash framework (themes, plugins) non-flake source
nur Nix User Repository (Firefox extensions) follows nixpkgs
sops-nix Encrypted secrets management follows nixpkgs
plasma-manager Declarative KDE Plasma config via home-manager follows nixpkgs
framework-system Official Framework hardware tool follows nixpkgs

Why multiple nixpkgs inputs? nixpkgs is the default unstable channel for most hosts. nixpkgs-unstable-small is used by electra so the Framework laptop can track faster-moving hardware support. nixpkgs-stable remains available as a package import escape hatch when a stable version is useful.

What does flake = false mean? The bash-it input has bash-it.flake = false;. This tells Nix the repository is not a flake. Nix fetches the source but does not evaluate it as a flake. Useful for pulling in non-Nix repositories whose files you want to use directly — the bash-it framework is symlinked into the home directory by home-manager.

outputs

The outputs function receives all inputs and returns what the flake provides.

outputs = { self, nixpkgs, nixpkgs-unstable-small, nixpkgs-stable, home-manager,
            claude-code-nix, codex-cli-nix, nixos-hardware, disko, bash-it, nur, sops-nix,
            ... }@inputs:

Notice the @inputs pattern (covered in Chapter 9). This destructures the inputs for direct use and captures the whole set as inputs for passing via specialArgs.

The makeNixosSystem helper

Rather than repeating the same setup for each host, your flake.nix defines a helper:

let
  makeNixosSystem = { pkgs ? nixpkgs, hmFlake ? home-manager, configName, isServer }:
    pkgs.lib.nixosSystem {
      specialArgs = { inherit inputs; };
      modules = [
        ./hosts/${configName}              # Host entry point
        sops-nix.nixosModules.sops         # Encrypted secrets
        hmFlake.nixosModules.home-manager  # Home-manager integration
        ({ config, ... }: {
          home-manager.useGlobalPkgs = true;
          home-manager.useUserPackages = true;
          home-manager.backupFileExtension = "backup";
          home-manager.sharedModules = [ inputs.plasma-manager.homeModules.plasma-manager ];
          home-manager.users.${config.nixosConfig.primaryUser} = import ./home/${config.nixosConfig.primaryUser}.nix;
          home-manager.extraSpecialArgs = {
            inherit inputs;
            isServer = isServer;
            flakeRepo = config.nixosConfig.flakeRepo;
            primaryUser = config.nixosConfig.primaryUser;
            userEmail = config.nixosConfig.userEmail;
          };
        })
      ];
    };

Key points:

  • specialArgs = { inherit inputs; } makes inputs available to every NixOS module
  • hmFlake.nixosModules.home-manager integrates home-manager into the NixOS build
  • sops-nix.nixosModules.sops provides encrypted secrets management
  • home-manager.sharedModules loads plasma-manager so home/plasma.nix can declaratively configure KDE Plasma
  • home-manager.extraSpecialArgs passes inputs, isServer, and portability values to home-manager modules
  • isServer allows home/nimmo.nix to skip desktop-only packages on server hosts
  • flakeRepo, primaryUser, and userEmail come from nixosConfig.* options (defined in modules/common/default-config.nix)

Host definitions

nixosConfigurations = {
  electra = makeNixosSystem {
    pkgs = nixpkgs-unstable-small;
    configName = "electra";
    isServer = false;
  };

  lena = makeNixosSystem {
    configName = "lena";
    isServer = false;
  };

  vega = makeNixosSystem {
    configName = "vega";
    isServer = true;
  };
};

Each host differs in two important ways:

  • nixpkgs input — electra uses nixpkgs-unstable-small; lena and vega use the default nixpkgs
  • isServer flag — vega is true; home-manager uses this to skip desktop-only packages

The Lock File: flake.lock

When you run nix flake update, Nix resolves each input URL to a specific Git revision and records it in flake.lock:

{
  "nodes": {
    "nixpkgs": {
      "locked": {
        "lastModified": 1770107345,
        "narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "4533d9293756b63904b7238acb84ac8fe4c8c2c4",
        "type": "github"
      }
    }
  }
}

Key fields:

  • rev — the exact Git commit hash. This is what makes builds reproducible.
  • narHash — a hash of the contents, used to verify integrity.
  • lastModified — timestamp for when this input was last updated.

Never edit flake.lock by hand. Use nix flake update to update all inputs, or nix flake update <input-name> to update one.

Why the lock file matters

Without the lock file, nixpkgs-unstable would resolve to whatever the latest commit is at the moment you build. Two builds an hour apart could produce different systems. The lock file pins the exact revision so every build is identical until you explicitly update.

Your flake.lock is committed to Git. This means you can see when inputs were last updated (git log flake.lock), revert a bad update by reverting the lock file, and your auto-update service commits lock file changes with a meaningful message — other hosts pick them up on their next pull.

Flake Commands

Check the flake

nix flake check

Evaluates all outputs and checks for errors. Does not build anything. Does not require root. Always run this before deploying.

Important: Flakes only see files tracked by Git. If you create a new .nix file, you must git add it before nix flake check will find it.

Update all inputs

nix flake update

Resolves every input to its latest revision and updates flake.lock.

Update a single input

nix flake update nixpkgs
nix flake update claude-code-nix

Useful when you want to update one tool without changing everything else.

Show flake info

nix flake show       # All outputs the flake provides
nix flake metadata   # Input URLs and their locked revisions

Show what changed

After running nix flake update, check what changed:

git diff flake.lock

You will see which revisions changed and can decide if you want to keep the update.

How Git and Flakes Interact

Flakes have a strict relationship with Git:

  1. Only tracked files are visible. If a file is not tracked by Git (not staged or committed), the flake cannot see it. This is the most common source of "file not found" errors.

  2. Dirty working tree is fine. You can have uncommitted changes to tracked files and flakes will use the current content. But untracked files are invisible.

  3. The flake.lock should be committed. This ensures reproducibility across machines and over time.

Your workflow should be:

  1. Create/edit files
  2. git add any new files
  3. nix flake check to validate
  4. git commit and git push