Cryptographic Hashes and Inputs

Updated

September 12, 2026

Cryptographic Hashes and Inputs

Two machines produce the same store path only if they evaluate the same derivation. The boring default is: every input — source, compiler, flags, patches, build script — is part of the hash, and a one-bit change is a new path, not an in-place upgrade.

Mental model

Traditional package managers treat ripgrep-14.1.0 as identity. It is a label. A rg built against glibc 2.38 is not the same program as a rg built against glibc 2.39.

Nix hashes the .drv file: a Nix-language serialisation of the builder, environment, and input store paths. The output path is /nix/store/<hash-of-that-drv>-<name>.

sources + toolchain + env + builder
            │
            ▼
         .drv file   ──SHA──►  output path
            │
            ▼
      sandbox build  ──►  bytes at that path

If the build is allowed to see the network, the time of day, or /usr/bin/cc, the hash is a lie. The sandbox exists so the hash means something.

Worked examples

Case 1: Instantiate before you build

Save as sample.nix:

# sample.nix
let
  pkgs = import <nixpkgs> {};
in
derivation {
  name = "desk-sample";
  system = builtins.currentSystem;
  builder = "${pkgs.bash}/bin/bash";
  args = [ "-c" "echo ok > $out" ];
}
nix-instantiate sample.nix

Output:

/nix/store/7vqw…-desk-sample.drv

No compiler ran. The hash is already decided. cat that .drv (it is text) and you will see the builder path, name, and system.

Case 2: One flag, new path

Save as opt.nix:

# opt.nix
{ cflags ? "-O2" }:
let
  pkgs = import <nixpkgs> {};
in
derivation {
  name = "desk-opt";
  system = builtins.currentSystem;
  builder = "${pkgs.bash}/bin/bash";
  inherit cflags;
  args = [ "-c" "echo $cflags > $out" ];
}
nix-instantiate opt.nix
nix-instantiate --argstr cflags -O3 opt.nix

Output:

/nix/store/aaaa…-desk-opt.drv
/nix/store/bbbb…-desk-opt.drv

-O2 versus -O3 is two derivations. Both can live on disk. Nothing was upgraded in place.

Case 3: The compiler is an input

nix-instantiate '<nixpkgs>' -A hello
nix-store -q --references $(nix-instantiate '<nixpkgs>' -A hello)

Output (abbreviated):

/nix/store/…-hello-2.12.1.drv
/nix/store/…-stdenv-linux.drv
/nix/store/…-source
…

hello does not hash “a C compiler.” It hashes this stdenv, which hashes this gcc, which hashes this source tarball. That is why a nixpkgs bump rebuilds the world: gcc moved, so everything that listed gcc as an input moved.

Case 4: Purity — the sandbox cannot see /usr

Save as impure.nix:

# impure.nix
let
  pkgs = import <nixpkgs> {};
in
derivation {
  name = "desk-impure";
  system = builtins.currentSystem;
  builder = "${pkgs.bash}/bin/bash";
  args = [ "-c" "/usr/bin/id > $out" ];
}
nix-build impure.nix

Output:

building '/nix/store/…-desk-impure.drv'...
/bin/bash: line 1: /usr/bin/id: No such file or directory
error: builder for '/nix/store/…-desk-impure.drv' failed

The sandbox does not contain /usr. That failure is the hash doing its job: if /usr/bin/id had been used, two machines with different id binaries would still have produced the “same” store path.

Case 5: Pin the input set with a lockfile

nix flake new ./desk-pin
cd desk-pin
nix flake lock
nix hash path flake.lock

flake.lock records the git revision and NAR hash of nixpkgs. Two laptops with the same lock file instantiate the same .drv hashes. Without the lock, nixpkgs is a moving label.

The trap

The trap is nix-build --impure (or builtins.currentTime, or filterSource that includes .git) so a “quick test” leaks host state into the hash. The path then differs between CI and a laptop for reasons that do not show up in git diff.

A second trap is trusting version numbers in pname / version as identity. They are labels inside the path. The hash is identity. Rename freely; never assume 1.0.0 means the same bytes as last week’s 1.0.0.

The boring rule

  • The .drv is hashed before the build. Instantiate to see identity.
  • Toolchains, flags, and sources are inputs. Changing any of them is a new path.
  • Keep the sandbox on. A build that needs /usr is not a Nix build.
  • Pin nixpkgs (flake lock or a pinned fetch). Do not build against a floating channel on a desk that ships artifacts.
  • Version strings are labels. Hashes are names.

Try this

  1. Add an environment variable desk = "window-a"; to sample.nix, instantiate, and confirm the .drv hash changed even though echo ok did not.
  2. Run nix-store --query --binding builder $(nix-instantiate sample.nix) and confirm it points at bash in /nix/store, not /bin/bash.
  3. Build opt.nix twice (-O2 and -O3), then diff -u $(nix-build opt.nix) $(nix-build --argstr cflags -O3 opt.nix).
  4. Search a .drv with grep gcc $(nix-instantiate '<nixpkgs>' -A hello) and note that gcc appears as a store path, not as the word “gcc”.