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:
-
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 rannix-channel --update. -
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; }makesinputsavailable to every NixOS modulehmFlake.nixosModules.home-managerintegrates home-manager into the NixOS buildsops-nix.nixosModules.sopsprovides encrypted secrets managementhome-manager.sharedModulesloads plasma-manager sohome/plasma.nixcan declaratively configure KDE Plasmahome-manager.extraSpecialArgspassesinputs,isServer, and portability values to home-manager modulesisServerallowshome/nimmo.nixto skip desktop-only packages on server hostsflakeRepo,primaryUser, anduserEmailcome fromnixosConfig.*options (defined inmodules/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 defaultnixpkgs - 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:
-
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.
-
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.
-
The flake.lock should be committed. This ensures reproducibility across machines and over time.
Your workflow should be:
- Create/edit files
git addany new filesnix flake checkto validategit commitandgit push