Fetchers and Fixed-Output Derivations

Updated

September 12, 2026

Fetchers and Fixed-Output Derivations

Every package starts as bytes from somewhere. The boring default is: fetchurl / fetchFromGitHub with an SRI hash = "sha256-…", and a hash mismatch is a stop, not a prompt to disable the check.

Mental model

A fixed-output derivation (FOD) may use the network. It must produce bytes whose hash you declared. The store path is a function of that hash. Mirrors can change; the hash must not.

Fetcher When
fetchurl A tarball or file at a URL
fetchFromGitHub owner + repo + rev (tag or commit)
fetchgit Arbitrary git remote
fetchzip A zip you want unpacked
fetchpatch / fetchpatch2 One patch file (fetchpatch2 on 26.05)

Language vendor hashes (vendorHash, cargoHash) are the same idea: FODs for Go modules / crates.io.

url + rev          hash you wrote
    │                  │
    ▼                  ▼
 download  ──cmp──►  sha256  ──ok──►  /nix/store/<fod-hash>-src
                       │
                      fail → error: hash mismatch (got …)

Empty hash sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= is a probe. You run the build once, paste got, commit. Leaving the probe in git is how supply chain theatre happens.

Worked examples

Case 1: fetchurl with a probe hash

Save as fod.nix:

# fod.nix
{ pkgs ? import <nixpkgs> {} }:

pkgs.fetchurl {
  url = "https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz";
  hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
}
nix-build fod.nix

Output (shape):

error: hash mismatch in fixed-output derivation '/nix/store/…-hello-2.12.1.tar.gz':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-0P4s2aQwHzpwC3w3ys8ls6A6/JbuiJvTYdbBr9pQMnQ=

Paste got into hash, rebuild:

nix-build fod.nix
tar -tzf result | head

The path is now stable. Changing only the URL to a mirror of the same bytes keeps the path.

Case 2: fetchFromGitHub

Save as gh.nix:

# gh.nix
{ pkgs ? import <nixpkgs> {} }:

pkgs.fetchFromGitHub {
  owner = "NixOS";
  repo = "nix";
  rev = "2.35.2";
  hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
}

Use a tag or full commit. rev = "master" is a moving label; the hash will mismatch the moment master moves, or worse, you use lib.fakeHash and skip the mismatch.

Probe, paste, pin. Prefer rev = "abc1234…" (full git sha) for anything the desk ships.

fetchFromGitHub is fetchzip of the GitHub archive URL. fetchgit talks git and can set leaveDotGit (almost always off for src). hash is SRI; sha256 = "0…" still works and is the old base32 — new listings use hash. Do not set both.

Case 3: nix-prefetch so you do not fake-hash in a loop

nix-prefetch-url https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz
nix hash convert --hash-algo sha256 --to sri "$(nix-prefetch-url https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz)"

or:

nix-prefetch-url --unpack https://github.com/NixOS/nix/archive/2.35.2.tar.gz

fetchFromGitHub unpacks; the hash is over the unpacked tree (outputHashMode = "recursive"), not the .tar.gz bytes. That is why a fetchurl hash and a fetchFromGitHub hash for “the same release” differ. nix-prefetch-url (packed) vs nix-prefetch-url --unpack / nix-prefetch-github (unpacked) — match the fetcher.

nix-prefetch-github NixOS nix --rev 2.35.2

fetchpatch2 hashes the applied patch more reliably than fetchpatch (line endings, GitHub PR URLs). Prefer it on 26.05. leaveDotGit = true on fetchgit makes the hash include .git — usually wrong for src; use it only when the build runs git describe.

fetchurl { name = "hello-2.12.1.tar.gz"; … } sets the store basename; without name, Nix uses a hash of the URL and debugging nix-store -q --references is noisier.

Case 4: A package src

Save as pkg.nix:

# pkg.nix
{ pkgs ? import <nixpkgs> {} }:

pkgs.stdenv.mkDerivation rec {
  pname = "hello";
  version = "2.12.1";
  src = pkgs.fetchurl {
    url = "https://ftp.gnu.org/gnu/hello/hello-${version}.tar.gz";
    hash = "sha256-0P4s2aQwHzpwC3w3ys8ls6A6/JbuiJvTYdbBr9pQMnQ=";
  };
}

The hash in this listing is illustrative. Always use the got from your fetch. If GNU republishes the tarball, the mismatch is the news.

Case 5: lib.fakeHash is a development aid

hash = pkgs.lib.fakeHash;

It expands to a dummy SRI. Fine in a dirty worktree. git grep fakeHash must be empty on main.

The trap

The trap is sha256 = ""; or hash = lib.fakeHash in production so CI “stays green” when upstream retags. You are no longer pinning bytes. You are pinning a URL, which is not a pin.

The other trap is hashing the wrong thing: fetchurl hash on a GitHub archive URL while the package uses fetchFromGitHub (unpacked). Always mismatch. Prefetch with the same fetcher you will use.

A third: rev = "v1.2.3" on a repo that moves tags. Pin the git sha, put the tag in version.

The boring rule

  • Every src is a FOD with a committed SRI hash.
  • Probe once (fakeHash / AAAA…), paste got, commit.
  • Pin rev to a git sha (tag in version only), never master.
  • git grep fakeHash is empty on the default branch.
  • Prefetch with the same fetcher (packed vs unpacked) you ship.
  • fetchpatch2 for remote patches. No leaveDotGit unless git describe is the build.

Try this

  1. Run Case 1, paste the real hash, rebuild, then flip one character of the hash and read the mismatch.
  2. nix hash convert --help and convert a hex sha256 to SRI.
  3. Fetch the same hello tarball with fetchurl and, unpacked, with fetchzip / fetchFromGitHub if applicable. Confirm the hashes differ.
  4. git grep -n fakeHash on a flake repo you maintain. Delete any hit on the main branch.