Packaging Anti-Patterns

Updated

September 12, 2026

Packaging Anti-Patterns

Most “Nix is slow / undeterministic” reports are the same five habits. The boring default is: fix these before adding Crane, uv2nix, or another overlay file.

Mental model

Smell Usual cause
CI ≠ laptop <nixpkgs> or unlocked nixpkgs#
Every commit rebuilds gcc src = ./. includes docs / .git / target/
Eval takes minutes import-from-derivation
go on PATH ≠ CI nix-env / Home Manager compiler
Infinite recursion final.pkg.overrideAttrs
Password in nix why-depends secret in a derivation

dream2nix / flake-parts are not the default. They paper over these if you let them. Delete the anti-pattern first.

Worked examples

Case 1: <nixpkgs> in production

Bad:

# configuration.nix — do not
{ pkgs ? import <nixpkgs> { }, ... }: {
  environment.systemPackages = [ pkgs.hello ];
}

Fix — pin nixos-26.05:

# flake.nix
{
  description = "Desk";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

  outputs = { self, nixpkgs }: {
    nixosConfigurations.desk = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [ ./configuration.nix ];
    };
  };
}
nix flake metadata

One nixpkgs node. Two nodes means a follows is missing.

Case 2: Fat src

Bad: src = ./.; at the monorepo root.

Fix:

# desk-api.nix
{ pkgs, lib }:

pkgs.buildGoModule {
  pname = "desk-api";
  version = "1.0.0";
  src = lib.fileset.toSource {
    root = ../.;
    fileset = lib.fileset.unions [ ../apps/desk-api ../go.mod ../go.sum ];
  };
  vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
}
nix-build
echo x >> README.md
nix-build --dry-run

If gcc is in “will be built,” the filter failed.

Case 3: IFD

Bad:

# do not
let
  src = pkgs.fetchFromGitHub {
    owner = "desk";
    repo = "mod";
    rev = "abc123";
    hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
  };
in
import "${src}/default.nix"

Eval builds src before it can parse default.nix. Flakes + IFD is a foot-gun.

Fix: copy default.nix into this repo, or add a flake input and callPackage.

Case 4: Compiler on the profile

Bad:

nix-env -iA nixpkgs.go
nix profile install nixpkgs#rustc

Fix: devShells.default with the same go / rustPlatform as buildGoModule / buildRustPackage.

# flake.nix fragment
{
  devShells.x86_64-linux.default = pkgs.mkShell {
    packages = [ pkgs.go pkgs.gopls ];
  };
}

nix profile for jq, not compilers. which go outside nix develop should fail (or be a distro go you must not use).

Case 5: Overlay recursion and secrets

Bad: hello = final.hello.overrideAttrs (_: { }) → infinite recursion.

Fix: prev.hello.overrideAttrs.

Bad:

# do not
stdenv.mkDerivation {
  pname = "desk-secret";
  version = "1.0";
  src = ./.;
  password = "hunter2";
}

or writeText "env" "TOKEN=…".

Fix: sops-nix path at runtime. Check:

nix path-info -r ./result | xargs -I{} grep -l hunter2 {} 2>/dev/null || echo 'token not in closure'

nix path-info -r must not grep the token.

The trap

The trap is papering over these with dream2nix / flake-parts / a mega-overlay. The new tool then IFDs, too. Delete the anti-pattern first. The other trap is fakeHash left on main — every CI run is a FOD mismatch.

The boring rule

  • Pin nixos-26.05. Commit flake.lock.
  • Filter src. No IFD on the hot path.
  • No nix-env. No compiler in home.packages.
  • Override this package from prev.
  • No secret bytes in $out.

Try this

  1. git grep -n '<nixpkgs>' and git grep 'src = \./\.'.
  2. git grep nix-env.
  3. nix flake metadata — a single nixpkgs lock node.
  4. --dry-run after a README-only edit on a filtered package.
  5. git grep -nE 'password = "|TOKEN=' -- '*.nix'.