APIs, integration & security — in depth

Nix Derivation Hash Inputs and Build Traceability

How Nix's input hashes ensure build traceability but not bit-for-bit reproducibility.

Editor at Large · · 11 min read
Cover illustration for “Nix Derivation Hash Inputs and Build Traceability”
Nix Derivation Anatomy · September 10, 2026 · 11 min read · 2,529 words

A Nix derivation hash is not a cache key with delusions of grandeur. It's a cryptographic commitment: every byte of every input that goes into a build gets folded into a single fixed identifier before the build ever runs, which means the store path itself is a claim about provenance, not just a lookup address. Understanding what that hash actually covers, and where it stops covering things, tells you exactly how much a given /nix/store path can be trusted, and where the remaining gaps live.

Start with what a derivation actually is. It's not a build script, and it's not a Makefile pretending to be portable. A .drv file is a specification of a single execve call: the exact path to an executable, the arguments passed to it, the environment variables it sees, and the set of inputs it's allowed to touch. Nix evaluation takes a .nix expression and compiles it down to this .drv, which lands in the store before anything gets built. Only after that file exists does nix-build actually run the process it describes. That two-phase split, evaluation then execution, is what lets Nix pre-compute the output path before a single line of source code compiles.

Look at a real one. The GNU hello .drv names bash-5.2-p15, stdenv-linux, and hello-2.12.1.tar.gz.drv as inputDrvs, each pinned to a specific store path, each of those paths itself the product of a hash. The build script gets hashed. The environment variables get hashed. Even the builder executable, bash sitting at some /nix/store/...-bash-5.2-p15/bin/bash path, is part of the input set. Change a compiler flag, swap a patch, tweak one environment variable, and the resulting hash changes, which means the store path changes. The old path never gets touched. /nix/store/sbldylj3clbkc0aqvjjzfa6slp4zdvlj-hello-2.12.1 isn't a record of what came out of a build. It's a commitment made before the build started.

How the default input-addressed model achieves repeatability, and why that is a more precise claim than reproducibility

Nix's default model is input-addressed, sometimes called intensional. The store path comes from hashing the recipe, the full closure of declared inputs, not from hashing the output bytes themselves. That distinction sounds pedantic until you see what it buys you and what it doesn't.

What it buys you is repeatability: give Nix the same .drv, and it will always attempt the identical build, and if a result already sits at that path, it skips the work entirely. That's the whole caching mechanism, and it's hash-only. There's no notion of "the last build of this project," no build history, no state tracked outside the store path itself. Two unrelated teams can run builds on the same machine without stepping on each other, because identity is entirely a function of the hash. No queues keyed to project names, no locking around shared state.

What repeatability does not mean is bit-for-bit reproducibility. As Farid Zakaria's July 2026 post notes, Nix, by default, was never designed to guarantee that two machines building the same .drv produce identical output bytes. That's a deliberate scope decision, not a bug someone forgot to fix. The traceability payoff is real: every path in the store carries its full input fingerprint, so you can always trace backward and ask what recipe produced a given artifact. What you cannot assume, without more work, is that the bytes another machine would produce from that same .drv are identical to yours.

Fixed-output derivations and how external sources get anchored into the hash chain

Every build eventually has to reach outside the closed system and pull something in from the internet, a tarball, a vendored blob, a git archive. That's a trust problem on its face: a URL is a pointer, not a promise. The same URL can serve different bytes today than it served last year, and nothing about a URL string stops that.

Fixed-output derivations solve this by inverting the usual rule. A normal derivation is named by the hash of its recipe. An FOD is named by the hash of its expected output. The derivation hardcodes what the fetched content should hash to, and the sandbox loosens just enough to allow network access during that one fetch, but the resulting bytes get checked against the hardcoded hash before anything enters the store. Mismatch, and the build dies loud, with an error showing exactly what hash was expected against what hash actually came down the wire. There's no silent pass here, no quiet substitution.

That's what makes the chain trustworthy end to end. Every upstream source that ever enters a Nix build carries a content hash that's now a permanent part of the derivation graph, and swapping that source out from under the build is structurally blocked, not just discouraged by convention. FOD output paths then feed downstream as ordinary inputs, so the hash of the original tarball is embedded in every artifact built on top of it. Malka et al., writing at ICSE-NIER in 2024, found this graph-level pinning enabled 99.99% reproducibility of build environments across 7,010,516 packages spanning 200 historical Nixpkgs revisions, and 99.94% of packages from a Nixpkgs revision six years old could still be rebuilt. That's not evidence the chain works on one machine. That's evidence it holds across years.

Where the sandbox enforces hermeticity, and where it introduces a hidden variable

The sandbox's job is narrow and specific: stop a build from reading anything it wasn't explicitly given. No ambient network, no host filesystem peeking through, no system libraries floating in from wherever the machine happens to keep them. For a standard mkDerivation build, that's enforced tightly. The build sees only what the derivation names.

Here's the crack in that story, laid out in Farid Zakaria's July 2026 post: sandbox-paths, the extra filesystem paths mounted into the sandbox at build time, are configured outside the .drv file. They're not part of the hash. Two machines can run the exact same .drv, byte-identical, with different sandbox-paths configured, produce different output, and both builds will still claim the same store path. The sandbox-paths default isn't even a fixed constant across a Nix version number, it's a compile-time property baked into the specific Nix binary someone happens to be running. Two people on "the same version" of Nix can have meaningfully different sandboxes underneath them.

This is a quieter failure mode than the obvious sources of non-determinism, like reading /dev/random or stamping a build with the current date. A build that checks whether some injected file exists and branches on that can pass --check and look byte-reproducible on the machine where it was authored, while silently diverging somewhere else. The .drv is the declared recipe, but it turns out not to be the complete description of the environment the build actually runs in. Sandbox configuration is an undeclared co-input, sitting just outside the hash chain.

Import-from-derivation makes the audit problem worse in a related way. IFD lets evaluation itself depend on the output of a build, which turns the evaluation graph from something static into something dynamic, and dynamic graphs are hard to reason about in CI. Forbidding IFD in continuous integration keeps evaluation a pure function of whatever files are actually committed to the repo, which is exactly the property an auditor wants.

Content-addressed derivations and what moving to output hashes would change

Content-addressed derivations flip the addressing scheme around: instead of naming a store path after the recipe, CA derivations name it after the hash of the output bytes themselves, moving Nix closer to the extensional model. It's an experimental feature, opt-in, available since Nix 2.4, and still marked experimental even now.

The payoff is early cutoff. If some dependency changes its recipe but happens to produce byte-identical output, everything downstream can skip rebuilding, because the output hash hasn't moved even though the input hash has. That can't happen under input-addressing, where any change anywhere in the recipe forces a new path regardless of what the output actually looks like. CA derivations also change who has to trust whom: if two independent builders produce the same output hash from the same derivation, that agreement is itself a form of verification, and users sharing a store don't need to trust each other's build processes, just the matching hash.

What CA derivations don't fix is the sandbox problem from the section above. The output hash gets checked after the build finishes, which confirms what came out, but it does nothing to guarantee what happened during execution. And the feature's road hasn't been smooth: it isn't currently usable in Lix, which has a full rewrite of the feature planned, so anyone building on an alternative Nix implementation needs to check compatibility before betting a workflow on it. Still, the direction is clear: identity tied to the artifact rather than the recipe is the foundation the provenance tooling later in this piece actually needs.

Flakes and the lock file as the reproducibility contract across a team

flake.nix declares what a project depends on. flake.lock pins every one of those inputs to an exact revision and an exact NAR hash, and that lock file is the actual contract, machine-readable, that every developer and every CI job evaluates against. Every entry in it resolves down to a derivation that's hashed the same way everything else in this piece has been describing, so the lock file is really just another root sitting on top of the same chain.

What flakes add on top of bare Nix is standardization: pinned inputs in one consistent format, evaluation that behaves the same across machines, and a single entry point CI can consume without bespoke per-project setup. Onboarding collapses to almost nothing: a new contributor installs Nix, clones the repo, runs direnv allow, and the entire environment builds itself from the locked graph with no manual dependency-wrangling step, a workflow that new contributors can follow without manual dependency-wrangling.

Graham Christensen of Determinate Systems has pointed to Dependabot's support for Nix flakes as a signal that Nix has moved past niche tooling into something dependency-update automation treats as a first-class, auditable artifact rather than a developer convenience nobody outside the project understands. That matters for drift, too: diffing flake.lock between two points in time shows exactly which input hashes moved, with no guessing involved. Run CI against a committed lock file, and CI is evaluating the identical graph a developer evaluates locally. The environment itself stops being a variable in the equation.

What the derivation graph exposes for software supply chain auditing and SBOM generation

Every Nix build is, by construction, a fully introspectable graph: source, compiler, linker, build script, every environment variable, all addressable by hash. That graph doesn't stop at the language boundary either. It includes the entire native closure, glibc, openssl, whatever system-level libraries actually got linked in, not just the packages a language manifest happens to declare. That's a materially richer picture than most language-ecosystem SBOMs manage on their own.

Nix doesn't natively speak SPDX or CycloneDX, but the graph is the ground truth those formats are trying to describe, and tooling has grown up to bridge the gap. sbomnix, from tiiuae, generates SBOMs from a flake reference or a store path, and ships with nixgraph for visualizing the dependency graph, vulnxscan for scanning SBOMs against known vulnerabilities, and a provenance tool that produces SLSA v1.0 compliant attestation files in JSON for any flake or derivation, with CycloneDX and SPDX outputs published as release assets. Determinate Systems offers FlakeBOM, a CLI that generates CycloneDX 1.5 SBOMs for flakes, and FlakeAudit, which evaluates those SBOMs against custom policy: flakeaudit scan writes advisory data pulled from NVD and osv.dev back into the SBOM, and flakeaudit compare diffs two SBOMs using Nix-specific metadata to answer precisely what changed between two releases. There's also nix-attest, aimed at SLSA provenance through Nix and GitHub Actions, built to answer one question cleanly: what were the evaluation-time inputs that produced this derivation? Determinate Systems separately offers Secure Packages, built on dedicated ephemeral infrastructure, distributed through FlakeHub, which holds FedRAMP High authorization.

The EU Cyber Resilience Act's Annex I requires a bill of materials in a machine-readable, commonly used format, kept current as software changes. The derivation graph paired with CycloneDX tooling isn't an approximate answer to that requirement, it's a fairly direct structural one, because the hash sitting in the SBOM's component field isn't a descriptive label somebody typed in. It's the same hash the store uses for deduplication, which means a consumer can verify a component against an actual store path without needing to trust whatever the SBOM's author claims about it.

The trust gap that derivation hashes cannot close on their own

A derivation hash proves one thing cleanly: consistency with a particular Nix evaluation, the same inputs, the same recipe, the same declared environment feeding into the same execve call. It does not, and structurally cannot, prove that the evaluator, the builder, or the local toolchain that ran the job behaved honestly while doing it.

A compromised build host can inject code mid-compilation, swap out a cached dependency, or replace the final output after the build step technically finished, and the .drv sitting on record never changes. The store path stays exactly the same. CI logs, signed checksums, provenance documents the pipeline emits on its own, all of that proves that some system made a statement. None of it proves the statement was true, a distinction the Kettle paper (arXiv:2605.08363) addresses.iv:2605.08363, April 2026) draws directly.

This isn't a flaw specific to Nix. It's the structural gap between controlling inputs and guaranteeing execution integrity, and any build system that ultimately trusts a build host to just behave carries the same exposure, full stop. Nix's grip on inputs and environment is tighter than most alternatives manage, which makes it a stronger foundation for a provenance strategy than most systems offer out of the box. But strong input control is a different layer of the stack than execution integrity, and Nix, however well it does the former, doesn't reach the latter on its own. The sandbox-paths problem from earlier is really a quieter version of this same gap: even with no compromised host anywhere in the picture, an undeclared input can still shift what a build produces without moving the hash at all.

Hardware-attested builds and what closing the execution-integrity gap looks like

Closing that gap means moving the guarantee out of software logs and into hardware that can attest to what actually ran. Kettle, from Confidential.ai and described in the arXiv paper referenced above, is built around that idea: using confidential computing so the build environment itself produces a cryptographic attestation of what code executed, not just a signed record of what someone claims executed afterward.

That's a different guarantee than anything a derivation hash offers, and it's not a replacement for one either. A .drv still has to define what the correct inputs are before there's anything meaningful to attest to. Hardware attestation answers the question Nix's model leaves open: did the machine that built this artifact actually run the process it claims to have run, on the hardware it claims to have run it on, without a build host quietly lying about the whole thing. Input control and execution integrity are separate problems, and a build artifact deserves real trust only once both of them are actually closed.

Sources

  1. Kettle: Attested builds for verifiable software provenance
  2. Reproducibility of Build Environments through Space and Time
  3. The Nix sandbox is a hidden input
  4. github.com
  5. nixos.wiki
  6. determinate.systems

More in Nix Derivation Anatomy