Pure Eval and Impure¶
Overview¶
Flake commands evaluate in pure mode by default. The result of a flake evaluation is fully determined by declared inputs—locked in flake.lock—not by ambient filesystem state, environment variables, or NIX_PATH. That hermetic eval plays well with caching and makes “what went into this build?” auditable.
This page is the flake-focused deep dive. The Flake concept page summarizes the idea; language-level rules live in purity boundaries. When something fails under pure eval, prefer fixing the root cause—declare an input, keep paths inside the flake tree, pin a fetch with a hash, pass system explicitly—over permanently relying on --impure.
Verified against Nix 2.34.x (CLI 2.34.8 / stable manual). Flake installables and nix flake / nix eval require the experimental flakes and nix-command features; flags and defaults can still change until those features stabilize.
Details¶
Pure vs impure (flake boundary)¶
| Concern | Pure flake eval (default) | With --impure |
|---|---|---|
| Declared flake inputs + lockfile | Allowed | Allowed |
| Paths inside the flake source tree | Allowed (see Git note below) | Allowed |
Absolute host paths, ~/…, paths outside the tree |
Rejected | Allowed (mutable) |
builtins.currentSystem / currentTime / nixPath / storePath |
Disabled (attribute missing / pure-mode error) | Available |
builtins.getEnv |
Returns "" |
Real environment |
<nixpkgs> / NIX_PATH |
Not used for flake deps; angle-bracket lookups fail | Usable (e.g. with --expr) |
Unpinned fetches / unlocked getFlake |
Rejected | Allowed |
Global nix.conf defaults pure-eval to false, but flake-based installables turn pure evaluation on for the flake evaluation path. Non-flake workflows can opt in with nix eval --pure-eval or pure-eval = true in config. The CLI documents --impure as allowing access to mutable paths and repositories.
--file / -f implies --impure, because the file path itself is a mutable reference.
What pure evaluation restricts¶
Pure evaluation mode (pure-eval in nix.conf) ensures expressions depend only on explicitly declared inputs, not on undeclared external state. When active, Nix generally:
- Restricts filesystem and network access to content pinned by cryptographic hash (store paths, hash-pinned fetches, locked flake inputs) and to the flake’s own source tree.
- Disables impure constants:
builtins.currentSystem,builtins.currentTime,builtins.nixPath, andbuiltins.storePath(see thepure-evalentry). Accessing those attributes fails with “attribute … missing” (or an equivalent pure-mode error forstorePath). - Neutralizes ambient env reads:
builtins.getEnvreturns the empty string rather than the process environment. - Rejects home-directory path literals (
~/…— including under~/.config/…), absolute host paths outside allowed inputs, and angle-bracket lookups like<nixpkgs>that depend onNIX_PATH.
Typical error wording (Nix 2.34.x): access to absolute path '…' is forbidden in pure evaluation mode (use '--impure' to override), or the path '~/.config/…' can not be resolved in pure mode.
Relative paths and the flake tree¶
Under flakes, a bare path installable (., ./subdir) is copied into the store as a git+file: or path: input before evaluation. Relative path literals that stay inside that tree (for example ./modules/foo.nix, builtins.readFile ./data.txt) are fine. Paths that escape the tree (../outside, absolute /home/…, or ~/…) are impure and fail unless you pass --impure.
Prefer path literals over string interpolation of paths. Antiquoting a subdirectory (e.g. "${./host}/file.nix") copies only that subdirectory into the store; a later ../ import then leaves the allowed tree and triggers the same pure-eval path error. Keep modules as path values, or anchor with self / flake inputs—see flake.nix schema.
Flakes vs classic impurity¶
Classic channel workflows resolve dependencies through NIX_PATH and impure imports:
Flakes ignore NIX_PATH for dependency resolution. Dependencies must appear in the flake’s inputs block and be pinned in the lockfile—see inputs and outputs and migration from channels. Symbolic names like nixpkgs#hello use the flake registry, not <nixpkgs> search paths.
Git flakes and the source tree¶
For a path inside a Git repo, Nix treats the flake as a git+file: input and copies only files indexed by Git (committed or git add-ed). Untracked files and paths matched by .gitignore are invisible to evaluation—a common gotcha when a new file exists on disk but was never staged. Stage or commit files you need the flake to see before building.
To include the working tree as a plain directory (including untracked files), use an explicit path:… flake reference instead of a bare path. That changes how the source is copied; it does not by itself relax pure-eval rules for builtins or NIX_PATH.
Fetches and fixed-output derivations¶
At evaluation time, unpinned network fetches (fetchTarball without a hash, unlocked fetchGit, and similar) are rejected under pure eval. Pin content with a revision and/or sha256 (or use flake inputs instead). Unlocked builtins.getFlake refs likewise need --impure.
At build time, downloads go through fixed-output derivations (FODs): fetchurl, fetchFromGitHub, and related helpers must declare an outputHash. That is controlled impurity—the network may be used, but only for bytes matching the declared hash. Eval purity and build-time FOD fetches are related but separate concerns; see purity and reproducibility.
IFD is a separate switch¶
Import from derivation (IFD) lets evaluation realise a derivation and then import / readFile its output. Pure flake eval does not disable IFD. IFD is gated by allow-import-from-derivation in nix.conf (default true in Nix 2.34.x) and the CLI overrides --allow-import-from-derivation / --no-allow-import-from-derivation.
Hydra and many CI setups set allow-import-from-derivation = false so nix flake check and similar eval-only jobs cannot trigger builds mid-evaluation. Pure eval controls which ambient paths and builtins are visible; IFD controls whether eval may build to continue. Both can fail a flake check for different reasons.
Escape hatch: --impure¶
When you truly need impure evaluation—legacy expressions, reading host paths, one-off debugging, or tooling that reads the environment—pass --impure on the Nix command (for example nix build --impure .#package, nix eval --impure .#value, nix flake check --impure). Documented on the experimental nix / nix eval / nix flake check pages (Nix 2.34 stable manual).
Treat --impure as a temporary escape hatch, not a project default. Long-term fixes:
- Replace
<nixpkgs>/builtins.nixPathwith flakeinputs. - Keep imports and
readFiletargets inside the flake tree (or declare another flake input). - Pass
systemexplicitly (e.g. attribute paths underpackages.<system>for packages, apps, and devShells) instead ofbuiltins.currentSystem. - Pin fetches and commit flake.lock.
- Move host-specific values into flake inputs or module options rather than
getEnvor~/….
Examples¶
Failing <nixpkgs> inside a flake¶
Corpus fixtures: broken-flake.nix (fails) and fixed-flake.nix (locked input).
# flake.nix (broken under flake pure eval)
{
outputs = { ... }: {
packages.x86_64-linux.default =
(import <nixpkgs> { system = "x86_64-linux"; }).hello;
};
}
<nixpkgs> depends on NIX_PATH, which flake evaluation does not use. Fix by declaring nixpkgs as an input:
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }: {
packages.x86_64-linux.default =
nixpkgs.legacyPackages.x86_64-linux.hello;
};
}
Run nix flake lock (or any flake command) to populate the lockfile, then nix build .#default. Requires experimental nix-command and flakes (Nix 2.34.x).
Absolute / home paths vs in-tree paths¶
# flake.nix — in-tree read is pure-eval-safe once the file is in the flake source
{
outputs = { self }: {
note = builtins.readFile ./note.txt;
# host = builtins.readFile /etc/hostname; # fails: absolute path
# cfg = builtins.readFile ~/.config/nix/nix.conf; # fails: ~ in pure mode
};
}
nix eval .#note # OK under pure flake eval
nix eval --impure --expr 'builtins.readFile /etc/hostname'
builtins.currentSystem under flakes¶
# flake.nix — fails: attribute 'currentSystem' missing
{
outputs = { self }: {
value = builtins.currentSystem;
};
}
Prefer an explicit system in the output path (packages.x86_64-linux.…) rather than --impure plus currentSystem.
When --impure might appear¶
A third-party helper that still calls builtins.getEnv or imports an unpinned / out-of-tree path may only work with nix build --impure .#…. Document that requirement for local experiments, but refactor toward declared inputs before shipping to CI. Inline --expr with <nixpkgs> similarly needs --impure (or an -I / flake pin). Separately, if CI sets allow-import-from-derivation = false, fix IFD in the expression—--impure alone does not re-enable IFD.
References¶
- Nix manual —
pure-eval(nix.conf) — pure evaluation mode settings (stable / 2.34.x) - Nix manual —
nix(installables /--impure) — flake path vspath:;--impurefor mutable paths;-fimplies--impure(experimentalnix-command) - Nix manual —
nix flake— flake format, inputs, outputs, and lock file (experimentalflakes) - Nix manual —
nix eval—--impure,--file/--expr(experimental) - Nix manual —
nix flake check— eval checks under pure flake eval;--impureescape - Nix manual —
allow-import-from-derivation— IFD gate (separate frompure-eval) - nix.dev — Flakes (concept) — defaults to pure mode; Git staging requirement
See also¶
- Purity boundaries — language-level builtins, paths, eval vs build impurity
- Import from derivation — IFD vs pure eval;
allow-import-from-derivation - flake.nix schema — inputs, outputs,
self, path wiring - Packages, apps, and devShells — explicit
<system>output paths - flakes (experimental feature) — enabling
flakes/ pairing withnix-command - Inputs and outputs — declaring locked flake dependencies