A hands-on technical reference for platform engineers operationalizing Nix-based SBOMs, CVE remediation, and supply chain controls in real C

Content-Addressed Derivations in Nix Experimental Features

Content-addressed paths let Nix skip rebuilds and verify outputs without trusting the cache.

· 11 min read · Updated
Cover illustration for “Content-Addressed Derivations in Nix Experimental Features”
Nix Derivation Anatomy · September 13, 2026 · 11 min read · 2,424 words

Content-addressed derivations change what a Nix store path actually promises. Instead of encoding a recipe (the builder, its arguments, its dependency graph), the path now encodes the result: what got built, not how it got built. That inversion unlocks two things Nix has never done before: skip rebuilds it can prove are unnecessary, and let strangers share a cache without trusting each other. Most writeups treat this as a performance feature, a way to save compute. That framing gets the priority backwards: the trust model is the real change, and the compute savings are what fall out of it.

What content-addressed derivations change about how store paths are computed

Diagram: Input-Addressed vs. Content-Addressed: What the Store Path Encodes. Visualizes: Show a before/after contrast between two store path models.

In the standard Nix model, a derivation's output path is a hash of its inputs: the builder, the arguments passed to it, every dependency in the closure. Change any of those, however trivially, and the resulting path changes too, whether or not the built artifact differs by a single byte. The store path is a statement about provenance. It tells you where something came from, not what it is.

That's reliable, and it's wasteful in a specific, well-documented way. Nix has no mechanism to know, before rebuilding, whether an input change will actually change the output. A clean example of the cost: swap a source URL for a mirror, and every downstream package that depends on it rebuilds, even though the downloaded file is bit-for-bit identical to the one at the old URL. The rebuild is safe. It's also entirely unnecessary, and at the scale of Hydra or a large nixpkgs CI pipeline, that unnecessary work adds up fast.

Nix already carves out one exception: fixed-output derivations. An FOD's path depends only on the expected content hash of the fetched artifact, not on the URL or any other metadata describing how it got there. The only form of content-addressing stabilized in Nix today is this one, and its use proves the concept works, at least for the narrow case of fetches. The ca-derivations experimental feature generalizes the same idea to arbitrary build outputs.

Under ca-derivations, a content-addressed output's path depends on the derivation's name, the output spec's chosen hashing method (NAR hashing being the common case), and the actual content of what got built. The builder, the environment, the specific inputs, all of it gets ignored for path computation. Two derivations with different inputs that happen to produce identical output now resolve to the same store path.

One distinction is worth being precise about. Fixed CA outputs pre-declare the expected hash and fail the build if reality doesn't match, the same way FODs work today. Floating CA outputs compute the hash after the build finishes, without pre-declaring anything. Floating is the part ca-derivations actually adds, and it's fully sandboxed, just like input-addressed builds. Not having a pre-declared hash doesn't mean the builder gets network access or any other impure privilege.

Turning it on takes two steps: add experimental-features = ca-derivations to nix.conf, then opt individual derivations in with __contentAddressed = true in mkDerivation, or flip config.contentAddressedByDefault = true to opt an entire nixpkgs instance in. The flag is off by default, and using it means accepting that behavior can still change in backwards-incompatible ways.

Early cutoff: skipping downstream rebuilds when outputs haven't changed

Early cutoff is the ability to stop a build chain the moment Nix can prove a step's output already exists somewhere. It's the headline benefit here, and it's worth understanding exactly, not approximately.

Take Haskell as the canonical case. Add a comment to source code, and the compiler technically sees a changed input. GHC strips comments before it does anything meaningful with the code, though, so the compiled output is identical to what existed before the edit. Under input-addressing, that identical output still gets a new path, because the input hash changed, and everything downstream of that component rebuilds regardless of whether a single byte in the resulting binary differs. Under content-addressing, Nix computes the output hash, notices it already has that exact content-addressed path in the store, and stops. Nothing downstream rebuilds.

This kind of skip is structurally impossible without content-addressing, not just impractical. Input-addressing has no mechanism for two different derivations to land on the same output path, so the question of whether the change actually mattered downstream never even gets asked.

The benefit isn't evenly distributed, and pretending otherwise misses the point. It scales with how wide and deep a package sits in the dependency graph. A compiler, a core C library, a libc wrapper: these sit underneath enormous numbers of downstream packages, and under input-addressing, they're exactly the ones that generate the most unnecessary rebuild cascades from trivial upstream changes. At the scale of a large nixpkgs CI pipeline, early cutoff has real potential to cut both compute time and cache storage. The actual motivation for the feature is not a side benefit tacked on for a changelog.

For individual developers and CI pipelines, the effect shows up as a faster inner loop. Reformatting code, editing comments, bumping metadata that doesn't touch build logic: none of that should cascade into rebuilding a dozen downstream packages. With CA derivations enabled, it stops doing so.

Trustless substitution: what changes about sharing and verifying build outputs

Under input-addressing, trusting a binary cache substitute means trusting the cache itself. The store path only tells you what recipe produced it, not what the resulting artifact contains, so verifying a substitute really means verifying the source: cache.nixos.org's signing key, for instance, rather than the artifact.

Content-addressing flips that arrangement. A CA store path is a cryptographic commitment to the content of the thing itself. Anyone who can compute and check that hash can verify the artifact independently of who built it or which cache it came from. As the Haskell.nix documentation describes it, CA derivations change Nix's trust model by letting multiple users share the same store without trusting each other.

Picture two CI machines and a developer's laptop, all building the same derivation. If all three produce the same content-addressed output, they're sharing a store path that any of them can verify on their own, with no central signing authority required to vouch for it. That's a genuinely different arrangement from the one Nix users work under today, where substituting from a shared cache means trusting whoever holds the signing key. Anyone still treating that distinction as cosmetic hasn't thought through what happens when the key gets compromised, or when the party holding it simply isn't trustworthy in the first place.

It also puts Nix's model in step with where supply-chain security work has been heading generally: provenance as a property of the artifact itself, verifiable independent of the pipeline that produced it, rather than provenance as a claim the pipeline makes about itself. Content-addressing gives Nix a clean, hash-based signal for whether two independent builds agree on the result. Agreement shows up as identical hashes. Disagreement shows up immediately, and it's auditable rather than silently absorbed into "trust the cache."

That last point matters more than it first appears. It creates a natural checkpoint for catching non-determinism in build toolchains, something that's genuinely hard to see under input-addressing, where two different builders producing two different binaries from the same inputs never even surfaces as a conflict. The system just picks one substitute and moves on, unaware that anything was ambiguous in the first place.

The non-reproducibility problem that CA derivations expose but do not solve

Diagram: The Reproducibility Trap: How CA Derivations Expose Non-Determinism. Visualizes: Illustrate the pathological failure mode introduced by CA derivations when a build is non-reproducible.

That checkpoint cuts both ways, though. Input-addressing hides non-reproducibility. Content-addressing drags it into view, whether anyone wants to deal with it or not, and this is the part of the feature most adoption conversations gloss over.

Here's the actual mechanism. If a build isn't bit-reproducible, two builders running the identical derivation can produce two different outputs, and under CA derivations, two different outputs mean two different store paths. Under input-addressing, that ambiguity never surfaces: both builders land on the same path (computed from inputs, not outputs), one substitute gets accepted, and nobody ever compares the actual bytes.

Under content-addressing, that same disagreement becomes an explicit conflict. The current implementation, described in the research as "naive," forbids fetching a path from the cache if the local store already has a different realisation for the same derivation output. The pathological case is concrete. A developer builds derivation A locally and gets output A.out. A CI machine builds the same derivation, non-determinism creeps in somewhere in the toolchain, and it produces a different output binary, then populates the shared cache with that. The developer's machine now refuses to substitute the downstream package that depends on A, because its local realisation of A doesn't match what's in the cache. The cache, for that developer, is dead weight.

Be blunt about what this means: CA derivations don't just benefit from reproducibility, they require it to function as advertised, and a package that isn't reproducible actively makes things worse under this model, not neutral. A non-reproducible package can force rebuilds that the old, pessimistic, input-addressed model would have simply avoided by never comparing outputs in the first place. This known, actively discussed limitation is a significant reason the feature remains experimental rather than stable, alongside other unfinished aspects of the implementation.

The failure mode compounds, too. A single non-reproducible package sitting low in the dependency tree, a C library, a compiler runtime, can cascade into cache misses across every package built on top of it. Teams weighing early adoption need a real answer to one question before anything else: are the packages that matter most actually reproducible, bit for bit, and how would anyone know without testing it directly?

Where ca-derivations sits in Nix's experimental feature landscape and its stabilization trajectory

Nix has gated features behind explicit flags since version 2.4. Before that, unstable behavior shipped without any signal that it was unstable, which left users discovering instability by accident rather than by choice. The flag system exists specifically so adoption is a deliberate decision, not a surprise.

An experimental feature in Nix can be changed arbitrarily, dropped entirely, or promoted to stable. Promotion isn't just a matter of the code being finished. It requires evidence of real-world use, a track record of fixed bugs, well-understood interactions with the rest of the system, and enough maintainer confidence to commit to backwards compatibility going forward.

ca-derivations is tracked toward stabilization under milestone #35 in the NixOS/nix repository, and bug report #12361, opened by contributor roberth on January 27, 2025, shows the feature is under active, ongoing work rather than sitting abandoned. It's available today in both stable and unstable Nix releases, but only behind explicit opt-in, at the Nix configuration level and, absent contentAddressedByDefault, at the level of each individual derivation.

It doesn't sit alone in the experimental feature list, either. dynamic-derivations extends the same underlying model into meta-build territory, enabling more dynamic dependency structures between derivations. git-hashing offers an alternative to NAR-based hashing, using Git's own hashing algorithm for content-addressed store objects. impure-derivations lets a derivation produce different output every time it runs via the __impure attribute, and an impure derivation cannot also be content-addressed; the two are mutually exclusive by design. And fetch-closure enables the fetchClosure builtin, which can fetch a store path closure from a binary cache, a feature that gets more useful as more of the ecosystem actually produces content-addressed paths to fetch.

The flag mechanism itself matters here as a design choice, not just a warning label. It functions as a firewall: teams can turn ca-derivations on and evaluate it in isolation, without it touching the rest of their Nix configuration. For most teams running stable nixpkgs today, fixed-output derivations already deliver the stabilized slice of content-addressing that matters most for fetching, and that's enough, full stop, unless there's a specific reason to want more. Teams in security-sensitive or compliance-heavy environments, where provenance and reproducibility carry real weight, have the clearest reason to track the milestone closely rather than wait for a general announcement.

What adopting ca-derivations requires in practice and how to think about timing

The minimum bar is straightforward and requires a Nix version that supports the feature, the ca-derivations flag set in nix.conf, and either __contentAddressed = true on the specific derivations that matter, or config.contentAddressedByDefault = true if the whole tree is going in at once.

Don't take the whole-tree route. Not yet, and for most teams, not for a long while. Nothing requires committing an entire package tree on day one, and given the reproducibility trap described above, nothing should. Enable it selectively, for the packages already known to be reproducible, and leave everything else on the existing input-addressed model.

Reproducibility is the real adoption gate here, not the feature flag. Teams that have already put in the work to make builds bit-reproducible, common in regulated industries, security tooling, and infrastructure-as-code shops, are positioned to see the benefit of early cutoff and trustless substitution almost immediately. Teams that haven't done that work yet should expect flipping the flag to make some CI scenarios worse before it makes anything better: the cache-invalidation pathology described above becomes a live problem rather than a theoretical one.

Packages already known to be output-invariant under the kinds of changes that happen constantly make the best starting point: compiled packages with deterministic build systems, packages that strip debug information as a matter of course, packages whose frequent changes are limited to metadata or comments rather than logic. Shared-store setups, several developers or CI agents all writing to the same Nix store, see the trust-model benefit most directly, since content-addressed paths let every participant verify what the others built without needing a shared signing key to lean on.

Declarative environment tooling built on top of Nix stands to gain the same way, since the whole value proposition of that kind of tooling rests on one guarantee: the environment specified is the environment received, and any two parties building from the same specification should be able to confirm, cryptographically, that they landed on the same result. CA derivations sharpen that guarantee rather than replacing it.

Given an active tracking issue and a milestone in visible progress as of early 2025, the sensible posture isn't to wait indefinitely for a stable release, and it isn't to flip the switch everywhere either. Turn the flag on in a non-production environment, watch which packages actually benefit from early cutoff, and find the non-reproducible packages that would cause friction before they cause it in production. That signal, fed back into reproducibility work now, is what makes the eventual stabilization worth something when it lands.

Sources

  1. Content-addressing derivation outputs - Nix 2.28.8 Reference Manual
  2. Experimental Features - Nix 2.28.8 Reference Manual
  3. rfcs/rfcs/0062-content-addressed-paths.md at master · NixOS/rfcs
  4. Content addressed derivations - Haskell.nix
  5. github.com
  6. wiki.nixos.org
  7. Implementing a content-addressed Nix
  8. Experimental Features - Nix 2.34.9 Reference Manual

More in Nix Derivation Anatomy