Skip to content

Nix / NixOS Footguns

Mistakes that break reproducibility, corrupt your store, or leave you unable to boot.


1. nix-collect-garbage -d Before Understanding GC Roots

You run nix-collect-garbage -d to free disk space. The -d flag deletes ALL old generations first, then runs GC. You lose system generations you intended to keep for rollback, and build results that took hours to compile because you didn't have GC roots pointing to them.

Fix: Before running nix-collect-garbage -d, run nix-store --gc --print-roots | grep -v /proc to see what's protected, and sudo nix-env --list-generations -p /nix/var/nix/profiles/system to see system generations. Use nix-collect-garbage (without -d) to only collect genuinely unreferenced paths. Keep ./result symlinks in place — they act as GC roots.


2. Mixing nix-env and Flakes

You use nix-env -iA nixpkgs.git to install a package globally, then manage everything else with flakes. The nix-env installation is not pinned to your flake's nixpkgs input — it uses whatever channel version is current. Your "reproducible" environment now has an unpinned system-level package that silently differs between machines.

Fix: Pick one paradigm per machine/role. Use nix profile install nixpkgs#git (flakes-aware) or manage everything declaratively in home.nix or configuration.nix. Audit stale nix-env installs: nix-env -q and nix profile list.


3. Forgetting to Commit flake.lock

You update flake.nix to add a new input, run nix develop (which generates/updates flake.lock), but don't commit flake.lock. A teammate pulls the repo, runs nix develop, and gets a different version of nixpkgs than you have because nix flake update resolved differently for them. Builds differ silently.

Fix: Always commit flake.lock alongside flake.nix. Treat it like package-lock.json — it is the reproducibility artifact. Add a CI check that flake.lock is committed and up to date.


4. Impure Builds Passing Locally, Failing in CI

Your derivation calls builtins.currentSystem or reads an environment variable at build time. It works on your x86_64-linux laptop. In CI (aarch64-linux or macOS), the derivation produces a different output or fails because the implicit system dependency wasn't declared.

Fix: Nix derivations must be pure: all inputs must be explicit. Never use builtins.currentSystem inside a derivation — pass system as a parameter from the flake's outputs. Use --impure flag only as a last resort for debugging, never in production pipelines.


5. nixos-rebuild switch with a Syntax Error Breaks Boot

You edit configuration.nix with a typo, run sudo nixos-rebuild switch, and it fails mid-apply. In some cases the boot entry is partially updated. You reboot and find yourself in an unbootable state or with no network because the partially applied config is broken.

Fix: Always run sudo nixos-rebuild dry-build first to check for syntax and evaluation errors before switch. Use sudo nixos-rebuild test to apply without updating the boot entry — if it's broken, reboot and you're back on the old config. For risky changes, use --rollback if the switch itself errors: sudo nixos-rebuild switch --rollback.


6. Wrong sha256 Hash Causes Opaque Build Failure

You add a fetchurl with an incorrect or placeholder sha256 hash. The error message says hash mismatch in fixed-output derivation and shows the expected vs actual hash, but beginners interpret this as a network or packaging bug and spend an hour debugging the wrong thing.

Fix: When adding any fetchurl, fetchFromGitHub, or similar, use nix-prefetch-url or lib.fakeSha256 (intentionally wrong, to get the correct hash from the error output on first build). After the first build attempt fails, copy the "got" hash from the error and paste it in. This is the intended workflow.


7. nix-shell Doesn't Exit the Shell When You Think It Does

You use nix-shell to enter a dev environment. Later you run exit, think you've left, and continue working. But you were nested: you were in a bash inside the nix-shell environment, exited that inner bash, and you're still in the nix-shell with all its env vars. Commands that "shouldn't work" still do.

Fix: Use echo $IN_NIX_SHELL to check if you're inside a nix-shell. Use direnv with use flake or use nix for automatic activation/deactivation when you enter/leave a directory — this avoids manual shell management entirely and is more reliable.


8. Overlays Applied in the Wrong Order Silently Shadow Packages

You define two overlays: one that patches openssl and one that builds curl against your patched openssl. If the overlays are applied in the wrong order, curl builds against the unpatched openssl and you don't notice because the build succeeds — curl just has different behavior.

Fix: Overlays in NixOS are merged in order: nixpkgs.overlays = [ overlay1 overlay2 ]. Overlays that depend on other overlays must come after them. Test overlay composition with nix repl before committing to a system config: pkgs = import <nixpkgs> { overlays = [ overlay1 overlay2 ]; }; pkgs.curl.


9. Cross-compilation Attribute Path Confusion

You're building for aarch64 on an x86_64 host. You use pkgs.myapp instead of pkgs.pkgsCross.aarch64-multiplatform.myapp. The result is an x86_64 binary silently. You flash it to an ARM device and get Exec format error at runtime.

Fix: For cross compilation, use pkgs.pkgsCross.<target>.myapp. Verify the result: file result/bin/myapp should show ELF 64-bit LSB executable, ARM aarch64. Set up a cross compilation matrix in CI to catch architecture mismatches before deployment.


10. Home Manager home.stateVersion Set Wrong

You set home.stateVersion = "23.11" in your home.nix but install home-manager 24.05. Or you upgrade home-manager but don't update stateVersion. This causes state migration logic to misbehave — some programs get wrong default paths, and you see cryptic activation errors.

Fix: home.stateVersion should match the home-manager release you installed initially. It is NOT the current version — it tracks the version at which you first set up home-manager for that profile. Only change it if you want home-manager to apply breaking migrations. Never set it to a future version.