Anti-Patterns¶
Overview¶
Language-level habits that hurt readability or reproducibility. Tracked against nix.dev best practices, the Nixpkgs overlays chapter, and Nix builtins docs—not ops mistakes (channels, nixos-rebuild flags). Prefer explicit scope (let / inherit), pinned inputs, shell-safe interpolation, and deep merges only when you mean them.
Details¶
Unquoted URLs¶
The language still accepts bare URLs (https://example.com). RFC 45 deprecated them; always quote URL strings.
Wide with¶
with pkgs; at file top (or nested with) dumps a huge attribute set into lexical scope. Readers cannot see where bare names come from; tools cannot resolve them without evaluating. Nested with makes origin ambiguous.
# avoid
with import <nixpkgs> { };
# … bare names from the whole set
# prefer
let
pkgs = import <nixpkgs> { };
inherit (pkgs) curl jq;
in
# use curl, jq, or pkgs.foo
Small list scopes (buildInputs = with pkgs; [ … ]) are less bad but still surprising under shadowing rules. To avoid with in lists: builtins.attrValues { inherit (pkgs) curl jq; }. Prefer let / inherit.
rec overuse¶
rec { … } puts every attribute in scope of every other. Shadowing an outer name with itself (let a = 1; in rec { a = a; }) yields infinite recursion that is hard to debug.
Or name the set and refer through that binding (argset.a). See rec and fixed points.
<nixpkgs> / lookup paths¶
<nixpkgs> resolves via $NIX_PATH — host state, often a channel tip that differs per machine. Fine for tiny examples; for real configs pin nixpkgs (flakes, fetchTarball + hash, npins, or a fixed NIX_PATH under VCS). See flake and purity boundaries.
Impure default Nixpkgs config¶
Even with a pinned path, import nixpkgs { } still reads host config/overlays from the filesystem by default. For reproducible imports, pass empty sets explicitly:
Unpinned fetchers¶
fetchTarball / tip-of-branch URLs without a content hash can change across runs (tarball-ttl, caches). Pin with sha256 (or use flake inputs). Details: import and fetch, purity boundaries.
src = ./. and directory names¶
Copying a path literal like ./. into the store names the store path after the parent directory. Different checkout directory names → different store paths and needless rebuilds.
See path and filesystem.
Shallow // (and overlays)¶
Attrset update (//) and overlay composition replace nested attrsets; they do not deep-merge. { a = { b = 1; }; } // { a = { c = 3; }; } drops b. When deep merge is intended, use lib.recursiveUpdate (see lib helpers). Overlay return values combine the same way — see overlays pattern.
# shallow — loses a.b
{ a = { b = 1; }; } // { a = { c = 3; }; }
# deep merge when that is the intent
lib.recursiveUpdate { a = { b = 1; }; } { a = { c = 3; }; }
Overlay final vs prev (and infinite recursion)¶
Per the Nixpkgs manual: use final for dependencies of packages you add or override (resolve against the finished set); use prev for the package being replaced and for helpers such as callPackage. Swapping them breaks consistency across the fixed-point set and often causes infinite recursion encountered.
# avoid — base package via final forces the attr being defined
final: prev: {
hello = final.hello.overrideAttrs (old: { /* … */ });
}
# prefer — replace via prev; take deps from final
final: prev: {
hello = prev.hello.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ final.someTool ];
});
myPkg = prev.callPackage ./my-pkg.nix { inherit (final) someDep; };
}
Also avoid forcing final in a top-level let of the overlay body before returning attrs—that can demand the fixed point too early. See overlays pattern and callPackage.
pkgs.extend / appendOverlays inside Nixpkgs¶
These recompute the Nixpkgs fixed point and are expensive. The overlays chapter says not to use them in nixpkgs itself; prefer composing overlays at import / nixpkgs.overlays instead.
IFD in flakes without need¶
Import from derivation (IFD) realises a store path mid-evaluation (import, readFile, … on a derivation output). Slow, sequential, and often banned in CI (allow-import-from-derivation = false). Pure flake eval does not disable IFD by itself.
# avoid — eval reads a built store path
let
generated = pkgs.runCommand "gen.nix" { } ''echo '{ x = 1; }' > $out'';
in
import generated
# prefer — commit or generate the .nix at edit time; eval only sources
import ./generated.nix
Ambient / mutated PATH¶
Builders and scripts that assume host /usr/bin tools, or prepend host paths onto $PATH, smuggle impurities into “hermetic” builds. Declare tools as derivation inputs and call them by store path (or a PATH built only from those inputs).
# avoid
buildPhase = ''
export PATH=/usr/bin:$PATH
make
'';
# prefer
nativeBuildInputs = [ pkgs.gnumake pkgs.gcc ];
# sandbox PATH comes from inputs; or call ${pkgs.gnumake}/bin/make explicitly
Unescaped store paths / strings in shell¶
Interpolating arbitrary strings into shell without quoting breaks on spaces and metacharacters, and can turn data into syntax. Use lib.escapeShellArg / escapeShellArgs (see lib helpers).
# avoid
''
install -Dm644 ${configFile} $out/etc/app.conf
${pkgs.curl}/bin/curl ${url}
''
# prefer
''
install -Dm644 ${lib.escapeShellArg configFile} $out/etc/app.conf
${lib.getExe pkgs.curl} ${lib.escapeShellArg url}
''
Store paths from packages are usually alphanumeric-safe, but user-facing paths, URLs, and filenames are not—escape by default when they become shell words.
Careless builtins.unsafeDiscardStringContext¶
Strings that interpolate derivations carry a string context so Nix tracks runtime/build dependencies. builtins.unsafeDiscardStringContext drops that tracking: the text of a store path can survive while the dependency is forgotten (wrong GC roots, missing build inputs, “file not found” at build time).
# avoid — path text without dependency edge
let
p = builtins.unsafeDiscardStringContext "${pkgs.hello}";
in
pkgs.writeText "note" "see ${p}"
# prefer — keep context so hello stays in the closure
pkgs.writeText "note" "see ${pkgs.hello}"
Only discard when you deliberately need a context-free string and understand the cost; prefer constructing from literals or hashes instead of stripping a live reference. Documented as unsafe in the Nix builtins manual.
Examples¶
Pinned vs floating nixpkgs input
# floating (example-only)
import <nixpkgs> { }
# pinned + empty host config (hash must match the URL)
import (builtins.fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/<rev>.tar.gz";
sha256 = "…";
}) { config = { }; overlays = [ ]; }
Fixed store name for local src
See also¶
- callPackage
- Overlays pattern
- lib helpers
- Import from derivation
- Pure eval and impure
- Derivation builtins (string context)
References¶
- Best practices — nix.dev
- Towards reproducibility: pinning Nixpkgs — nix.dev
- Language syntax (recursive sets, with) — Nix manual
- Builtins (
unsafeDiscardStringContext, string context) — Nix manual - Import From Derivation — Nix manual
- Overlays — Nixpkgs manual
lib.strings.escapeShellArg— Nixpkgs manual- RFC 0045 — Deprecate URL literals