flakes.md

Flakes

Before this, you'll want

  1. Paths and imports
Flakes25 min

Flakes are notoriously hard to explain. So we aren’t starting with flakes. First, build the common channel-based world they tidy up:

Your projectdrag or tap

Put the three independent Nix files onto the table.

Three files, each reaching outside the project on its own.

Drop a file onto the table, or tap it to place it.0 files
outside this projectyour nixpkgs channelmoving snapshot2025-09-04

Place every file, then hit the update below the table. Notice what just happened: none of your files changed, and yet every package version in them moved at once.

The missing piece was a receipt

Each of those files reached out to the moving <nixpkgs> channel living outside the project, which is how an awful lot of real Nix code still works.

Now, plain Nix can absolutely be pinned, so the problem was never “no flakes means no reproducibility”. The problem is that the shared pin is easy to leave out and very hard for the next person to spot.

A flake drags three jobs back into the project itself. flake.nix names what comes in and what the project offers, and flake.lock remembers exactly what came in.

The nice part is that one front door can then offer a NixOS system, a Home Manager home, a package, or a development shell. The Nix sitting behind it is still perfectly ordinary Nix.

Four things decide what a project builds, and they don’t all live in the same place, which is most of why flakes feel slippery at first.

MatchPair each one with what it is responsible for.

flake.nix
flake.lock
the registry
a channel

connect each item to its job

The top two travel with the repository. The bottom two do not, and that is the whole difference you just watched play out on the table.

Build the picture yourself

Toolboxdrag or tap

Start with an output, then complete its path to a CLI command.

Inputs

Outputs

CLI

flake.nix
flake.lock

No inputs locked yet

reviewed lock states
Drag a piece onto the table, or tap it to place it.0 paths
workbenchFlake editing mode

Start with an output. Its broken wire will tell you which input to go and fetch, and once you add that input, flake.lock unfolds underneath: the moving source on the left, the exact saved revision it landed on over on the right.

Try older and newer under the lock. Every saved revision moves together, and the package versions under each output follow along behind. Those buttons stand in for reviewing an older or newer flake.lock. Real lockfiles do not come with buttons, sadly.

Once the picture clicks, open flake editing mode under the table. Keep moving pieces around on the left and watch the same idea turn into Nix on the right.

Every project pins on its own

If I update my system, does the side project I haven’t touched in six months still build?

nixos-config
flake.nix
flake.lock
nixpkgs0f4a1c8
home-manager4b8e2d7
my-program
flake.nix
flake.lock
nixpkgs73aa986
work-api
flake.nix
flake.lock
nixpkgse7d5f31
flake-utils60cd2b3

Each project has its own flake.lock, so updating one moves only that one. The others stay on the revisions they were already building against.

Three projects, all of them depending on nixpkgs, none of them agreeing about which nixpkgs. Watch them for a moment and you’ll see the revisions never move together, because there is nothing they share for an update to travel along.

With a channel you depend on state that lives outside the project entirely, which is why one update back in the first step dragged every file along with it. A flake locks its dependencies inside the project, the way Cargo.lock or package-lock.json does. Update the machine on Tuesday and the work project carries on building against the revision it was happy with, until the day you go into that folder and ask it to move.

Change the input, keep the output

Toolboxdrag or tap

Connect nixpkgs 26.05 to a NixOS system.

Inputs

Outputs

CLI

flake.nix
flake.lock

No inputs locked yet

reviewed lock states
Start with the nixpkgs 26.05 input.0 paths

Build the old path first, then swap out only nixos-26.05 for nixos-unstable.

The NixOS output and nixos-rebuild both stay exactly where they were. What changes is the source feeding them, the exact revision it locks to, and every package version that follows from it.

Write down the board you built

That whole stable-to-unstable exercise, as one real file:

flake.nixFlakeFlake inputs and outputs are understood here, with flake-aware hovers and completion.Project filesNearby files from the example project are mounted, so relative paths and imports are real.

inputs.nixpkgs is the piece you swapped. nixosConfigurations.castle is the output that stayed put. nixos-rebuild --flake .#castle is the command that comes asking for it.

The exact revision is missing on purpose, because Nix writes that into flake.lock for you. You edit the moving intent in flake.nix, and the generated lock records what that intent actually resolved to.

One thing that catches people out later: if this flake is backed by Git, its source only includes files Git knows about. Add a new module and Nix may not see it until you git add it.

outputs is an ordinary function

There is no secret pipe hiding inside it. Nix resolves the inputs, calls a function with an attribute set, and keeps the attribute set that comes back. Which means publishing something new is just adding an attribute.

Try it

Publish the toolkit under packages.toolkit as well.

nix replevaluated as you typeNixA plain Nix expression. Nothing from nixpkgs or a module system is supplied.

The real nixpkgs value is enormous rather than a short string, and it arrives through that function in the same way this one does. Nothing about a flake’s inputs is more magical than a named function argument.

Ask the project what it offers

Since outputs are just attributes, something can go and read them. nix flake show does exactly that:

$ nix flake showgit+file:///home/yurii/my-program├───devShells│   └───x86_64-linux│       └───default: development environment└───packages    └───x86_64-linux        └───default: package 'my-program-1.0'

nix develop takes the default dev shell, nix build takes the default package. The command and the output name are the two ends of one path across the workbench.

GoalAsk a project what it offers, without building any of it.

Handy on somebody else’s repository too. It tells you what there is to build before you go guessing at attribute names.

Experimental

Official Nix still keeps flakes and the newer CLI behind the feature gate you turned on back in the CLI lesson. And now that you have seen what is behind it, that word on the label is worth a sentence.

“Experimental” means the interface can still change, which is a fair warning. It does not mean the community is sitting around waiting to find out, since an enormous number of projects already depend on flakes today.

What to keep

  • A common unpinned channel setup can move several independent Nix files at once.
  • flake.nix names moving inputs and the outputs a project offers.
  • flake.lock records the exact transitive input graph inside the project.
  • Locked input revisions determine the package versions the outputs produce.
  • outputs is an ordinary function, and conventional names connect its result to CLI tools.
  • The registry and a channel both live outside the project. flake.nix and flake.lock travel with it.
  • nix flake show reads a project’s outputs without building any of them.
  • Flakes are still experimental and widely adopted.

Useful links

NORMALCOURSE IN BETA