Disabling Implicit Dependencies in Nix for Full Hermeticity
Closing every route for undeclared dependencies to affect Nix builds.

Disabling implicit dependencies in Nix means closing every route by which a host machine can influence evaluation or a build. Full hermeticity requires declared, pinned inputs, restricted build access, and control over the configuration that enforces those boundaries. Nix has the tools for this, but getting reliable results means using them across the whole dependency graph and then testing build determinism on its own.
Why implicit dependencies make builds unreliable even when Nix is involved
Traditional package managers install tools and libraries into shared mutable locations such as /usr/bin and /lib. Build scripts can silently consume those resources through PATH, library lookups, or environment variables without declaring them as inputs. The script therefore describes only part of the build. Two developers can run the same commands and get different results because their machines supply different undeclared dependencies. The same machine can produce that discrepancy after its shared environment changes.
Hassanshahi et al. found that a large majority of the top commonly used Maven artifacts are not built using a transparent CI/CD pipeline. Users must therefore trust both the source and an unverified compilation environment. A source revision alone cannot explain an artifact if the tools and libraries used to compile it remain unknown.
Nix addresses this problem by storing packages under /nix/store at paths derived, in its default input-addressed model, from their declared build inputs. Those inputs include source, compilers, libraries, build flags, and declared environment variables. Changing an input produces a new path without overwriting the old one. However, an input-derived path cannot account for a dependency the build consumes outside that declaration. The architectural premise is therefore to reference dependencies by store path and prevent access to undeclared resources.
What hermeticity actually requires at the three layers of implicit input
Full hermeticity means sealing all three layers: the evaluation environment, the build environment, and the dependency graph. Each answers a different question about what can affect the result. Evaluation determines the derivation description, the build environment controls what its commands can reach, and references in the dependency graph can resolve to fixed or mutable inputs. Restricting only the compiler's environment leaves the other two open. Turning on the sandbox is therefore necessary but not sufficient.
At evaluation time, a Nix expression can become host-dependent before any build starts. builtins.currentSystem reflects the evaluating host. Similarly, builtins.getEnv can make the derivation depend on host environment variables. File references outside the declared source tree, including lookups through $NIX_PATH, can incorporate untracked material. These inputs change the recipe itself as well as how it runs.
At build time, the risk shifts to ambient access by compilers and scripts. Without isolation, they can read libraries on the host, use its temporary storage, or access the network. The dependency graph introduces a separate form of mutability when an expression leaves the revision unfixed by referring to a channel. The same shell.nix can then resolve to a different environment on another day or machine. That floating reference can still change when the build environment is restricted or the expression stays the same.
How flakes close the evaluation layer through pure mode and locked inputs
Flakes limit what the expression can read from its host during evaluation. Under pure evaluation, access to undeclared files outside the flake's directory tree is disallowed, and impure builtins such as builtins.currentSystem are disabled. builtins.getEnv remains available but returns an empty string. Evaluation must produce a recipe solely from declared inputs, independently of the machine running Nix.
The same restriction makes it safe to cache evaluation results. Before flakes, a .nix file could import other files by using relative paths, absolute paths, or $NIX_PATH. Without tracking every file touched, a cached evaluation could become stale while the expression appeared unchanged. A file outside the declared input set must be added to it or kept from affecting evaluation.
The flake.lock file prevents declared inputs, including transitive inputs, from changing by pinning them to specific Git commits and content hashes. With the expression, lock file, and explicit arguments held fixed, evaluation no longer depends on which channel revision a machine happens to have. Cross-platform flakes take the target system as an explicit input, replacing the host-derived value of builtins.currentSystem. The cost is that local dotfiles and ad-hoc paths cannot remain convenient but invisible inputs.
How the Nix sandbox seals the build layer and where configuration leaks in
Once evaluation produces a declared recipe, the build sandbox restricts the resources its commands can reach. Host compilers, libraries, and tools are not available simply because they are installed on the machine. The build must get its dependencies through declared store-path inputs. Network access is blocked by default during sandboxed builds, preventing ordinary build scripts from fetching mutable resources as they run. Source fetching must instead use the explicit, hashed fetching patterns discussed below.
The important limitation is that sandbox policy is not wholly contained in the derivation. The sandbox-paths setting controls which host paths are exposed inside the sandbox, and its default can be a compile-time property of the Nix binary. Two binaries with the same Nix version string can therefore have different effective sandbox defaults. As a result, the same derivation can behave differently without any change to its flake or recipe.
Nix consequently offers strong layered isolation whose effective environment can vary across installations. That distinction matters most when a security or compliance claim depends on the precise build boundary. Solarin et al. explain why determinism alone cannot establish artifact trust. An independent verifier must recover the source state, build environment, dependencies, and instructions that produced the artifact. Sandbox configuration belongs in that account because it governs what the recipe could actually access.
Disabling implicit dependencies in practice with specific primitives and patterns
Begin with pure evaluation by running nix flake check and nix build without --impure. Although that option can unblock exploration, it reopens evaluation-time access and can conceal dependencies that later fail in CI. Replace builtins.currentSystem with an explicit system argument passed through the flake's outputs function. In place of builtins.getEnv calls, pass explicit arguments to the derivation or use fixed store-path references. Any required file outside the flake directory must become a declared input with a pinned URL and expected hash.
At the build layer, confirm that sandbox = true is set in nix.conf. Linux enables sandboxing by default, while the Darwin sandbox has fewer restrictions, leaving a known gap for macOS-native builds. Declare every build-time host tool in nativeBuildInputs, including tools available on the ambient PATH. A build that succeeds without isolation but fails inside the sandbox exposes an undeclared dependency. Changes after the build need the same care, so fixupPhase patches should use store-path references in place of absolute host paths.
At the graph layer, pin nixpkgs and every other flake input to specific commits in flake.lock. A branch name or channel alias must not serve as the final locked reference. For source fetching, use fetchurl or fetchFromGitHub with an explicit sha256. The expected hash declares what the fetched content must be, so the build need not trust whatever the remote location sends.
The build tool itself also needs attention when Nix is embedded in another build system. In a Bazel workspace, bazel_local_nix from rules_nix bootstraps an ephemeral Nix from a pinned, checksummed official binary release. It runs unprivileged inside a bubblewrap user namespace and exposes results as ordinary Bazel artifacts. Determinism still requires pinning the nixpkgs revision in the installable. Pinning the bootstrap does not compensate for an unpinned dependency graph.
Why hermeticity and reproducibility are related but distinct
Hermeticity controls inputs, while reproducibility requires the build process to turn those inputs into identical output bytes. A sealed environment is necessary but not sufficient for that result. Embedded timestamps or non-deterministic ordering of linker inputs can change an artifact even when its declared dependencies remain fixed. Hassanshahi et al. found that 44 out of 100 randomly sampled Maven packages carrying the project.build.outputTimestamp property failed to rebuild successfully. That result shows that an explicit signal of reproducibility intent is not evidence that rebuilding succeeds.
Nix's default input-addressed model makes this distinction especially important. Identical declared inputs produce the same store path, but a non-deterministic script can produce different bytes at that path on different machines. Experimental content-addressed derivations use the output hash to set the path, so different hashes show when the output has changed. After sealing inputs, validate determinism by rebuilding on two machines or using nix build --check, which rebuilds an existing derivation and compares the outputs. A mismatch identifies output instability that dependency declarations alone cannot solve.
What fully hermetic builds enable for supply chain auditability and SBOM generation
A fully declared build gives those who review the supply chain a record of its dependencies. Store paths identify versioned inputs, and the closure supplies machine-readable material for a verifiable SBOM. This record connects an artifact to its dependency graph rather than leaving auditors to infer that graph afterward. Its value still depends on the boundary being enforced, since a recorded graph cannot explain an undeclared host resource. Hermeticity makes the record meaningful by limiting what the build could consume.
Tools can turn that record into distinct forms of evidence. sbomnix generates SBOMs from a Nix flake reference or store path, while nixgraph queries and visualizes dependencies. The provenance tool generates SLSA v1.0 compliant provenance attestation files for Nix flakes or derivations. SLSA's isolation and hermeticity requirements provide useful orientation, but a generated attestation does not remove the need to examine the effective build environment. The earlier distinction between a declared recipe and its enforced boundary remains central.
Carrying the input-derived hash into deployment metadata allows deployments containing a vulnerable store path to be identified without grepping logs or rescanning images. The EU Cyber Resilience Act mandates machine-readable SBOMs for products with digital elements, with that obligation enforceable from December 2027 and vulnerability reporting requirements beginning September 2026. Nix's closure model supplies the underlying dependency record as an output of the build process, making SBOM generation part of the build workflow.
Where hermeticity breaks down in practice and how to detect it before CI does
Hermeticity needs maintenance because projects continually acquire new ways to reach outside their declarations. A new script may invoke a host tool, a language toolchain may bring its own package fetcher, or a temporary --impure workaround may remain in place. These changes can make a local build appear healthy while weakening its input boundary. Detection therefore has to examine both successful builds and the conditions under which they succeed. Output comparison tests determinism, while isolation tests reveal reliance on ambient resources.
Run sandboxed builds in a clean VM or container without globally installed tools to expose dependencies supplied by a developer's machine. Review the runtime closure with nix-store --query --requisites for unexpected store paths, including unwanted system libraries or debug tooling. Running pure nix flake check evaluation on a machine with a different pinned nixpkgs channel can show whether it accidentally relies on the channel state of the host. These checks look for different ways a build can fail, since a missing package is only one possible cause. Together, they make boundary drift visible before it becomes a CI surprise.
The lasting control is organizational discipline around the declaration itself. Treat --impure as a debugging aid that must not reach the main branch, and make CI reject builds that require impure evaluation. A declarative environment manifest gives reviewers a human-readable account of tools, versions, and store-path references. Reviewers can then look at the dependency that changed and go beyond spotting a difference in the artifact. The manifest becomes a unit teams version and audit alongside the resulting binary.


