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

Derivation Naming Conventions and Version Metadata Accuracy

Nix treats package names as structural; authors treat them as cosmetic.

· 13 min read
Cover illustration for “Derivation Naming Conventions and Version Metadata Accuracy”
Nix Derivation Anatomy · September 18, 2026 · 13 min read · 2,915 words

A derivation in Nix is a pure function: it takes store objects as inputs, runs a sandboxed process that can read only what it's told to read, and writes only to the outputs it's told to write. That sandboxing is the whole point of the system's reproducibility guarantee. But the guarantee depends on something far more mundane than sandboxing: getting the name right. Get the naming and version metadata wrong, and Nix doesn't just get confused, it computes a different store path, silently, and every downstream consumer inherits that error without any warning that something went sideways.

Every derivation requires certain core attributes, including at minimum a name and a target system. The.drv file that results is an intermediate specification, roughly analogous to a compiled object file in a C toolchain, recording the information needed to reproduce the build. Nothing from the author's ambient shell leaks in. That isolation is the foundation of the reproducibility guarantee. The store path itself follows a fixed format: /nix/store/<hash>-<name>, something like /nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname, as laid out in the Nix Pills documentation. The hash portion is computed solely from the input derivations, which means the name string, version included, is a direct input to that hash computation. Change the version string and you don't get a warning label slapped on the same package. You get a different path entirely.

That's the tension running through this entire piece. Authors tend to treat naming as cosmetic, something to tidy up after the logic works. Nix treats naming as structural. Authors tend to treat naming as cosmetic, something to tidy up after the logic works, while Nix treats naming as structural, and that mismatch produces friction visible at every layer of the toolchain, from a single misspelled attribute to the entire SBOM pipeline downstream of a build.

The four namespaces that define a package's identity

Ask a Nix user what a package is "called" and you'll get at least four different, defensible answers. There's the attribute name, something like nixpkgs.ocamlPackages.pycaml, which is what tools like nix-shell and nixpkgs search interfaces actually key on. There's the derivation name, which is the name attribute baked into the .drv. There's the resulting store path. And there's the package version, which may or may not be legible from any of the above. NixOS Discourse has documented this fragmentation directly: these four things are related but not identical, and there is no guarantee that they will move in lockstep.

The attribute name carries the most authority for tooling purposes, but it isn't glued to a single derivation over time. Across nixpkgs releases, the same attribute path can resolve to a different derivation entirely, with no explicit version bump required in the consuming expression. That's a quiet mechanism, and it's exactly how lock files and SBOMs accumulate drift without anyone touching a version string on purpose.

The practical fallout lands on any tool that parses names to infer identity: CVE scanners, SBOM generators, cache lookup logic. Each one has to know which of the four namespaces it's actually reading, and most don't make that explicit anywhere in their documentation or output. The NixOS Wiki lists informal suffix conventions, things like -bin, -fresh, -still, -wrapped, and -unwrapped, but these are conventions. Nothing enforces them, and nothing guarantees a reader can decode capability or provenance from a suffix alone. A human glancing at a store path is guessing. A scanner doing the same thing is guessing at scale.

Two authoring-time failure modes: silent attribute typos and the digit constraint

Two distinct failure patterns occur at the point of authoring, and they behave nothing alike.

The first is silent. mkDerivation does not validate the argument names it receives, a fact documented in ayats.org's Nix lecture materials. Misspell version as versoin, and Nix silently drops the attribute without complaint. It just drops the attribute. The resulting derivation builds fine, produces a store path, and that path's name simply lacks a version component. No error, no warning, nothing to catch in review unless someone happens to eyeball the generated name closely. This is particularly dangerous in monorepos where derivation stubs get copy-pasted across dozens of packages and version strings get hand-edited afterward: one dropped character, and the mistake is visible only downstream, as a wrong cache hit, a mismatched SBOM entry, or a CVE scanner that comes back clean because the version string it needed to match against was never there in the first place.

The second failure mode is loud, eventually, but not soon enough. Nix requires that the version portion of a derivation name start with a digit, a constraint documented in a Replit engineering blog post updated in 2021. Authors who lean on commit SHAs as version identifiers run into this constantly, because a short SHA beginning with a letter (say, a3f9c21) violates the rule outright. Replit's own migration story is instructive here: the team published their blog post on Nix versioning, and only after publication did an external reader, Travis Cardwell, flag that the digit-start requirement posed a problem for their versioning approach. Replit updated the post to account for it. The constraint is syntactic, so Nix will throw a real, visible error, unlike the silent typo case, but that error appears during the build process rather than being caught earlier. The constraint is syntactic, so it may not surface until the build is actually attempted.

Both failures trace back to the same root: the naming contract in Nix is implicit. Nobody hands a new nixpkgs contributor a spec sheet for what a valid name looks like. They learn it from breakage, one build failure or one silently missing version string at a time.

Feature-flag naming inconsistency's effect on derivation variant identity

Optional features across nixpkgs are typically exposed as boolean flags, following a pattern like withPAM ? true feeding into buildInputs = lib.optional withPAM pam. Reasonable enough in isolation. The trouble starts when you look across the tree: the same capability, PAM support, appears variously as withPAM, withPAMSupport, enablePAM, and usePAM, depending on who wrote that particular expression and when. Nixpkgs issue #148730, opened in December 2021, documents exactly this problem, and as of the sources reviewed here, it remains open.

One informally proposed fix is a supportFoo convention applied uniformly to both external dependencies and internal optional features. It hasn't been adopted as policy, and it's contested: an alternative modeled on autoconf's --with versus --enable split has also been floated, without resolution either way. In the meantime, the inconsistency is just there, baked into thousands of package expressions.

The consequence for naming is direct. Variant suffixes meant to encode which features a build includes are inconsistent across the package set, so any tool trying to infer capability from a derivation name is working from unreliable signal. Overlay authors feel this most acutely: turning a capability on or off system-wide means chasing down a different flag name for every package that offers it, because there's no uniform surface to target with a single override. This is a structural risk, not a developer-experience gripe to file away for later. It means two derivations built with entirely different capability sets, one with PAM support compiled in and one without, can end up with names that provide no reliable signal about which features are present.

Keeping naming consistent within a single package is a solvable problem, mostly a matter of discipline and code review. Keeping it consistent across architectures and across time, inside a flake-based project with multiple contributors, is a much harder one.

How flakes close and don't fully close the gap between version strings and resolved artifacts

Flakes give Nix projects a standard structure, per-input version pinning, and a lock file, flake.lock, that shares pinned state across every contributor working on a project. The NixOS Wiki documents this as one of the primary motivations for the flakes design. Practically, flake.lock records the exact commit hash of every external input, and that's the mechanism that finally bridges the gap between "the version string written in a derivation" and "the actual resolved artifact that string is supposed to describe," a point that makes byte-for-byte reproducibility achievable given the same flake.lock across machines and time.

That's real progress. With a committed lock file, the version string in a derivation name becomes something checkable against a cryptographically pinned source, which is what makes byte-for-byte reproducibility achievable given the same flake.lock across machines and time.

What flakes don't fix is anything from the two prior sections. A silently dropped version attribute is still silently dropped. A commit SHA that violates the digit-start rule still violates it. Flakes pin the value, they don't validate that the value is the right one or even a well-formed one. So a wrong version string, once flakes enter the picture, just becomes a wrong version string pinned with total consistency across every machine that reads the lock file. Reproducible, and reproducibly wrong.

There's also a multi-architecture wrinkle that flakes introduce rather than resolve. Flake outputs are keyed per system, following the packages.<system>.<name> schema, so a project needs entries like entries for each supported system defined explicitly. Naming conventions have to be replicated consistently across every system tuple a project supports, and that's a common point of drift, especially once a team adds Apple Silicon or aarch64 Linux support after the fact and doesn't backport naming discipline to the new targets. The Nix manual specifies that the system attribute itself must be an exact string, "i686-linux" or "x86_64-darwin" and so on; a mismatch between the declared system and the actual build host produces a store path that can't be cached or shared, full stop.

Flakes remain officially marked experimental in Nix, based on sources current to this research, despite being the backbone of production pipelines at a considerable number of organizations already. Teams are building real infrastructure on a feature the project itself hasn't formally stabilized. That's not a defect exactly, but it is a governance gap: teams are building real infrastructure on a feature the project itself hasn't formally stabilized.

Even with flakes doing their job well, one open problem remains untouched: knowing not just what got built, but where the resulting store path actually came from.

The deriver field's long-standing limitation and its proposed replacement

Nix has had a mechanism for this for a long time, the deriver field, and it has never really worked as intended. The deriver field was meant to trace a store path back to the Nix expression that produced it. According to a comment from Eelco Dolstra on Hacker News, it fails at that job structurally, because hermetic evaluation wasn't achievable in the pre-flakes world, so there was never a reliable way to guarantee the recorded deriver matched what actually produced the artifact.

The practical result: the deriver value stored in a binary cache frequently doesn't match what a local evaluation produces, a problem that also affects fixed-output derivations. Instead of functioning as an audit trail, the field mostly generates confusion when someone goes looking for it.

That mismatch also produces a deeper flaw. Ericson2314, commenting on the same Hacker News thread, pointed out that derivers aren't unique in the first place: a single store path can legitimately have more than one valid deriver. A field that's supposed to answer "where did this come from" can't do that job reliably if the honest answer is sometimes "more than one place."

The community response is active: Nix PR #11749, referenced in that same October 2025 discussion, proposes real provenance tracking. Nix PR #11749, referenced in that same October 2025 discussion, proposes real provenance tracking: a provenance field, structured as a JSON object, attached to store paths and to .narinfo files, carrying signed claims about origin. The intent is to build formal build traces keyed on the resolved derivation itself, with signed metadata that can include something like "this came from this flake" as auditable, not just informal, context. A participant in that thread, tomberek, made the connection explicit: better provenance tracking would meaningfully improve SBOM generation, not just developer workflow polish. Difficulty naming a package and difficulty tracking its provenance are, in a real sense, the same difficulty viewed from two different angles.

Until PR #11749 or something like it lands and stabilizes, teams have no choice but to compensate with external tooling and old-fashioned discipline. What that discipline looks like in practice, and why it matters well beyond people who happen to use Nix day to day, is the next question.

Why metadata accuracy at build time is the only point where SBOM correctness can be guaranteed

Practitioner framing on this shifted meaningfully by 2026. A point now widely made in practitioner commentary: generating an SBOM was never really the hard part. The hard part is governance, continuous scanning, and having a remediation workflow ready for what the SBOM actually reveals once someone reads it. A related framing from Trantor's blog, April 2026, captures the shift in the question being asked of vendors: it used to be "can you provide an SBOM at all," and it's now "is the SBOM you provided accurate and actionable." Accuracy, not existence, is the bar now.

That shift connects directly back to derivation naming, and the connection isn't abstract. An SBOM entry that lists a package as mypackage-0.0.0 when the actual shipped version is something else entirely makes CVE matching useless on its face. The naming error doesn't stay contained to a cosmetic annoyance; it propagates straight into a security workflow failure, because a scanner matching against 0.0.0 will never find the CVE that applies to the real version.

There's a structural argument here too, and it favors generating SBOMs at build time rather than reconstructing them after the fact from a running system. SBOMs built from post-hoc production scans tend to miss build-time context entirely, according to Minimus's coverage: the transitive dependency resolution that happened during the build, the compiler flags that were actually set. That context exists at exactly one point in a package's lifecycle, the moment it's built, and nowhere else.

Regulation is catching up to this reality. The EU Cyber Resilience Act's first reporting obligations took effect in September 2026, requiring manufacturers to report actively exploited vulnerabilities and severe security incidents for any product with digital elements. Coverage on dev.to states that the broader machine-readable SBOM requirement, covering at minimum top-level dependencies, doesn't take effect until December 2027. The runway is short, and getting shorter.

Nix's content-addressable, deterministic store is, structurally, about as well suited to build-time SBOM generation as any system available. But that suitability is conditional: it only holds when derivation names and version metadata are accurate to begin with. Feed the system wrong inputs and it will faithfully, deterministically produce a wrong SBOM, with all the same guarantees of reproducibility now working against you instead of for you. When naming is correct and flake.lock A deterministic image digest can be signed with tools like Sigstore, and that signature then covers every dependency in the graph back to its source, not merely the top-level artifact, as cloudrps.com lays out.

The stakes of getting this right are not hypothetical. A study of 5,298 Docker builds by Malka et al. found that only 6.4% of rebuilt images matched the original's installed package versions exactly. That number is a useful reality check: genuine, verifiable reproducibility is rare in ordinary container workflows, and it's rare precisely because the structural discipline Nix can enforce, when names and versions are handled correctly, is absent almost everywhere else.

What correct derivation naming practice looks like across the package lifecycle

None of this calls for new tooling so much as it calls for treating naming as load-bearing, at every stage from first draft to CI.

At authoring time, the single highest-leverage habit is separating pname and version rather than setting name directly. Nix then auto-generates name as ${pname}-${version}, which keeps the version component explicit, visible in code review, and independently auditable rather than buried inside a single opaque string. Pair that with a habit of validating that any version string begins with a digit before it's committed, especially in packages built around commit SHAs or identifiers generated by some other build system upstream. mkDerivation will never warn about a typo in an attribute name, and a version silently missing from the generated store path name is often the only signal that something went wrong, arriving well after the fact. Overlay authors specifically should keep a running note of which feature-flag convention, withFoo, enableFoo, supportFoo, each upstream package actually uses, rather than assuming any uniformity exists across the tree, because it doesn't.

At the flake layer, commit flake.lock to version control, always. An uncommitted lock file means the version string sitting in a derivation name is pinned to nothing at all, which defeats the entire point of having one. Define outputs explicitly for every system tuple a project actually supports, x86_64-linux, aarch64-darwin, whatever else applies, and check that naming stays consistent across all of them before merging, not after someone notices a cache miss in production. Treat the system attribute as real metadata rather than boilerplate copied from an example: a mismatched system string doesn't just look wrong, it produces a store path that other machines can't cache or share, which quietly defeats the reproducibility guarantee the whole system exists to provide.

None of these habits are exotic. They're closer to hygiene than engineering. But hygiene is exactly the layer where Nix's guarantees actually live or die, because the store path hash doesn't care about intent, only about the string it was given.

Sources

  1. Our First Derivation - Nix Pills
  2. Naming convention for optional feature flags · Issue #148730 · NixOS/nixpkgs
  3. Dynamic version for Nix derivations | Replit
  4. Nix Derivation Madness | Hacker News
  5. Package naming conventions - NixOS Wiki
  6. The Nix lectures, part 2: Derivations
  7. Clarification on Package Names and Versions
  8. github.com

More in Nix Derivation Anatomy