Lock file discipline

Updated

July 30, 2026

Lock file discipline

Goal: Treat flake.lock as a deliberate pin, not a mysterious generated blob—update one input at a time, read diffs, and explain how locks interact with registries and multi-machine rebuilds.

Important

Blind nix flake update on a production host is how “nothing changed except everything.” Prefer staged, selective updates.

Why this chapter exists

Flakes solve channel drift only if you respect the lock file. Teams fail when:

  • Someone updates all inputs “because CI was green last week”
  • Lock files are gitignored (never do this for apps/hosts)
  • Two machines use different locks and argue about “Nix being unreproducible”
  • Registries silently redirect nixpkgs away from the flake’s pin

Lock hygiene is foundational for every later input (Home Manager, sops-nix, flake-parts, …).


Theory 1 — What flake.lock actually pins

For each flake input, the lock records roughly:

Field Meaning
type / owner / repo / url Where the input came from
rev Exact git commit (or equivalent)
narHash Content hash of the fetched tree
lastModified Metadata for humans/tools

Implication: two clones with the same lock should fetch the same input trees (modulo substituted vs rebuilt outputs later). Without the lock, nixos-26.05 is a moving branch tip.

Inputs vs follow-ons

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";
};

follows means Home Manager reuses your nixpkgs pin instead of its own nested lock entry as a separate copy. That reduces “two nixpkgs versions in one eval” confusion and often saves eval work.

Anatomy of a lock node (conceptual)

"nixpkgs": {
  "locked": {
    "lastModified": 1700000000,
    "narHash": "sha256-…",
    "owner": "NixOS",
    "repo": "nixpkgs",
    "rev": "abcdef…",
    "type": "github"
  },
  "original": {
    "owner": "NixOS",
    "ref": "nixos-26.05",
    "repo": "nixpkgs",
    "type": "github"
  }
}

original is policy (track nixos-26.05); locked is the decision (this commit).


Theory 2 — Update operations (precision toolkit)

Command Effect
nix flake lock Refresh lock metadata without changing intended refs (often no-op if already locked)
nix flake update Update all inputs to latest matching their url refs
nix flake update nixpkgs Update only the nixpkgs input
nix flake update home-manager Update only that input
nix flake metadata Show resolved pins / last modified

Modern CLI also supports updating multiple named inputs; check nix flake update --help on your Nix version.

Mental model

flake.nix   →  policy ("track nixos-26.05")
flake.lock  →  decision ("this commit of that policy")

Changing flake.nix URLs without updating lock can error or keep stale locks depending on change type—always re-lock intentionally and commit both files together when inputs change.


Theory 3 — How to read a lock diff

git diff flake.lock | head -200

Look for:

  1. Which input changed ("nixpkgs": { block)
  2. Old rev vs new rev
  3. narHash change (expected when rev changes)
  4. Nested inputs (if not follows)

Optional deep inspection:

nix flake metadata
nix flake metadata --json | jq '.locks.nodes.nixpkgs'

Map rev to a GitHub compare URL when investigating regressions:

https://github.com/NixOS/nixpkgs/compare/<old>...<new>

Nested duplicates

If you see nixpkgs_2 style nodes, a dependency brought its own nixpkgs. Prefer:

inputs.foo.inputs.nixpkgs.follows = "nixpkgs";

Then re-lock and confirm the duplicate disappears or shrinks.


Theory 4 — Pin strategies (pick a house rule)

Strategy When Risk
Track release branch (nixos-26.05) + lock commits Default for this book Branch tip moves; you choose when to update
Pin exact rev in flake.nix Freeze hard for a milestone Manual bumps; easy to forget security updates
Track nixos-unstable Bleeding edge lab Breakage rate higher
Multiple flakes (host vs tools) Advanced separation Operational complexity

Recommended for lab hosts: track nixos-26.05 (and matching HM release), commit lock, update one input per change window, rebuild + smoke-test before updating the next.

Security vs stability

Updating nixpkgs is how you receive package and module fixes. Never freeze a public-facing host forever “because it works.” Discipline is when and how, not whether.

Cadence idea Fit
Weekly selective nixpkgs on lab Learning hosts
PR-based lock bumps in CI Teams
Point release freezes for demos Talks / workshops
Unattended full flake update nightly Usually a bad default for hosts

Theory 5 — Registries (light literacy)

Flake registries map short names (nixpkgs) to URLs for ad-hoc commands like nix run nixpkgs#hello.

Concern Practice
Global registry Convenient for exploration
Your host flake Should use explicit inputs, not hope the registry matches
CI Prefer locked flake in repo over registry luck
nix registry list

If global nixpkgs points at unstable while your flake pins 26.05, that is fine for one-off nix run—as long as you never assume those are the same pin.

Registry vs input (summary)

nix run nixpkgs#hello     → registry resolution (may float)
nix build .#packages…     → your flake.lock
nixos-rebuild --flake .#  → your flake.lock

Theory 6 — Multi-machine and “reproducible”

Same lock + same system (x86_64-linux) → same evaluation of inputs. Realization may still:

  • Download substitutes vs build locally
  • Differ if impure eval sneaks in (builtins.currentSystem misuse, unpinned fetchTarball, IFD surprises)
  • Differ across architectures

Lock discipline is necessary, not always sufficient, for bit-identical everything—but it is the main lever you control daily.

Clone checklist

git clone <repo>
cd <repo>
git status   # flake.lock must be present
nix flake metadata
sudo nixos-rebuild switch --flake .#lab

Missing lock → everyone invents their own “26.05 tip.”


Theory 7 — Commit policy for locks

Do Don’t
Commit flake.lock with the change that needs it Gitignore lock for NixOS host flakes
One logical update per commit when possible Mix lock bumps with huge unrelated refactors
Note why you updated in commit body “chore: update” with no smoke test
Keep flake.nix + flake.lock paired in reverts Restore only one of the pair

Example commit body:

flake: update nixpkgs (26.05 tip)

Smoke: sshd active, rebuild switch OK, hello from devShell OK.

Revert pattern

git checkout HEAD~1 -- flake.lock
# if flake.nix inputs also changed, restore that too:
# git checkout HEAD~1 -- flake.nix flake.lock
sudo nixos-rebuild switch --flake .#lab

Theory 8 — Inputs that should co-move

Pair Rule of thumb
nixpkgs nixos-26.05 + HM release-26.05 Keep major/minor aligned
nixpkgs + sops-nix follows nixpkgs; update sops after nixpkgs if needed
flake-parts + nixpkgs Framework usually tracks independently; still follows when offered

Updating HM without nixpkgs (or the reverse) is a common source of obscure option errors. Prefer: update nixpkgs → rebuild → update HM → rebuild.


Worked example — Selective update narrative

Session A:  update nixpkgs only → rebuild lab → OK
Session B:  add home-manager input → lock appears → rebuild
Session C:  update home-manager only → HM activation issue → revert lock hunk

Reverting a bad lock is often faster than debugging three simultaneous input moves.

Worked commands

cp flake.lock /tmp/flake.lock.bak
nix flake update nixpkgs
git diff --stat flake.lock
nixos-rebuild build --flake .#lab
# if OK:
sudo nixos-rebuild switch --flake .#lab
# if bad:
cp /tmp/flake.lock.bak flake.lock
sudo nixos-rebuild switch --flake .#lab

Exercises

Exercise 1 — Snapshot current pins

cd /path/to/your-flake
nix flake metadata
cp flake.lock /tmp/flake.lock.bak
git status

Journal:

  1. List every input name and its locked rev (short hash is fine)
  2. Confirm flake.lock is tracked by git
  3. Note whether any input uses follows

Exercise 2 — Read the lock structure

nix flake metadata
nix flake metadata --json | jq '.locks.nodes | keys' 2>/dev/null || true

Open flake.lock and identify:

  • The root node’s inputs
  • The nixpkgs node’s rev and narHash
  • Any nested nixpkgs_2 style duplicates (candidate for follows)

Exercise 3 — Selective update of one input

Only if you accept rebuild churn. On a disposable lab host:

nix flake update nixpkgs
git diff --stat flake.lock
git diff flake.lock | head -120

Build before switch:

nixos-rebuild build --flake .#lab
sudo nixos-rebuild switch --flake .#lab

Smoke checklist:

systemctl is-system-running || true
systemctl is-active sshd
nix --version

If break: restore backup lock and re-switch.

Exercise 4 — Prove “update all” is louder

On a throwaway branch:

git checkout -b experiment/flake-update-all
nix flake update
git diff --stat flake.lock

Count how many inputs / revs moved. Then restore:

git checkout main   # or your real branch name
git branch -D experiment/flake-update-all

Write one sentence: why selective update is the default for hosts.

Exercise 5 — Document house rules

Create docs/lock-policy.md:

# Lock policy (lab)

- Track nixpkgs nixos-26.05
- Commit flake.lock always
- Update one input per change window
- Smoke: rebuild + SSH + one app path
- Revert lock on failure before deep debug
- Align HM release-* with nixos-*

Exercise 6 — Registry awareness (read-only)

nix registry list
nix flake metadata

Note if the registry’s nixpkgs rev differs from the flake’s. No need to change registries.

Exercise 7 — follows fix (if needed)

If you have (or add) a second input with its own nixpkgs:

inputs.foo.inputs.nixpkgs.follows = "nixpkgs";
nix flake lock
nix flake metadata --json | jq '.locks.nodes | keys'

Exercise 8 — Compare URL construction

Write the GitHub compare URL for your last nixpkgs bump (from Exercise 3) or a synthetic old→new pair from metadata history. Bookmark the pattern.

Exercise 9 — Impurity awareness list

List three things that can still differ with the same lock (substituters, arch, impure builtins). One sentence each.

Exercise 10 — CI anti-pattern note

In docs/lock-policy.md, add: “CI must not run nix flake update on every job; lock bumps are deliberate PRs.”

Exercise 11 — Paired restore drill

# simulate bad partial restore understanding
git show HEAD:flake.lock >/dev/null
# document: always restore flake.nix + flake.lock together when input URLs change

Exercise 12 — Multi-input update plan

Write the order you will use when adding Home Manager (next chapters): lock HM → follows → rebuild → only then update nixpkgs again.


Common gotchas

Symptom / mistake What to do
Everyone’s host differs after clone Missing or uncommitted flake.lock
follows not set → two nixpkgs Add inputs.foo.inputs.nixpkgs.follows = "nixpkgs"
Updated lock, forgot rebuild switch / boot to activate
CI uses nix flake update every run Pin lock in repo; update in deliberate PRs
“I’m on 26.05” but lock is months old Expected until you update; schedule updates
Restored flake.nix but not lock Always restore the pair when debugging inputs
Private input auth fails after update Token/SSH access issue, not lock format
HM release ≠ nixpkgs release Align release-26.05 with nixos-26.05
Reviewed only flake.nix in PR Always review lock diff too
Massive lock + feature PR Split: lock bump PR vs feature PR

Checkpoint

  • Can explain what rev + narHash buy you
  • Can update one named input and read the lock diff
  • Have a written house lock policy
  • Know how to revert flake.lock quickly
  • Understand registry ≠ flake pin
  • follows planned or applied for companion inputs
  • Know why full nix flake update is loud on hosts
  • Commit policy treats lock as first-class

Journal (optional)

Write three personal gotchas before continuing—especially any surprise nested nixpkgs or registry mismatch.