Creating and Composing Overlays

Updated

September 12, 2026

Creating and Composing Overlays

You need a patch, not a nixpkgs fork. The boring default is: final: prev: { … }, prev.pkg.override / overrideAttrs, final for siblings, and a short overlay — not a private nixpkgs.

Mental model

final: prev: {
  deskGreet = prev.writeShellScriptBin "desk-greet" "echo desk";
  hello = prev.hello.overrideAttrs (old: { pname = old.pname; });
}
Arg Meaning
prev Package set before this overlay. Override this package from prev.
final Package set after all overlays. Use for other packages this overlay also defines.

final.hello.overrideAttrs when you meant prev.hello is infinite recursion.

Apply:

import nixpkgs { overlays = [ overlayA overlayB ]; }

or NixOS:

# configuration.nix fragment
{
  nixpkgs.overlays = [
    (import ./overlays/desk.nix)
  ];
}

List order is apply order. Later overlays see earlier ones through final. A third overlay that also sets hello wins if it uses prev from after the second — or recurses if it uses final.hello. Keep one overlay file per concern (overlays/hello.nix, overlays/desk-scripts.nix) and import the list.

Worked examples

Case 1: Add a script to pkgs

Save as overlay_demo.nix:

# overlay_demo.nix
let
  deskOverlay = final: prev: {
    deskGreet = prev.writeShellScriptBin "desk-greet" ''
      echo "Welcome to the desk infrastructure"
    '';
  };
  pkgs = import <nixpkgs> { overlays = [ deskOverlay ]; };
in
{
  name = pkgs.deskGreet.name;
}
nix eval --impure --file overlay_demo.nix --json

(--impure because <nixpkgs>. In a flake, pass nixpkgs from inputs — no impure.)

Output:

{"name":"desk-greet"}

Case 2: Compose two overlays

Save as compose_overlays.nix:

# compose_overlays.nix
let
  overlayA = final: prev: { servicePort = 8080; };
  overlayB = final: prev: {
    serviceUrl = "http://127.0.0.1:${toString final.servicePort}";
  };
  pkgs = import <nixpkgs> { overlays = [ overlayA overlayB ]; };
in
{ url = pkgs.serviceUrl; }
nix eval --impure --file compose_overlays.nix --apply 'x: x.url'

Output:

"http://127.0.0.1:8080"

overlayB reads final.servicePort so it sees overlay A’s binding.

Case 3: overrideAttrs from prev

# hello-overlay.nix
final: prev: {
  hello = prev.hello.overrideAttrs (old: {
    pname = "desk-hello";
  });
}

The output path name changes. Downstream pkgs.hello is your patched one if they take hello from the same pkgs.

pkg.override { enableFoo = true; } is for callPackage arguments. overrideAttrs is for the derivation attrset (postPatch, version). Using overrideAttrs to flip a callPackage flag does nothing. Read the package file: if it is { stdenv, enableFoo ? false }:, you want override.

Case 4: Infinite recursion (the foot-gun)

# bad-overlay.nix
final: prev: {
  hello = final.hello.overrideAttrs (old: { });
}
nix eval --impure --expr 'import <nixpkgs> { overlays = [ (import ./bad-overlay.nix) ]; }' -f '<nixpkgs>' hello --apply 'p: p.name'

You will get infinite recursion (or a hang/error). Use prev.hello.

Case 5: Flake overlay export

# flake.nix fragment
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
  outputs = { self, nixpkgs }: {
    overlays.default = final: prev: {
      deskGreet = prev.writeShellScriptBin "desk-greet" "echo desk";
    };
  };
}

Consumers: overlays = [ desk-tooling.overlays.default ]; with follows on nixpkgs.

The trap

The trap is final.pkg.override…. Recursion.

The other trap is a 2000-line overlay that copies half of nixpkgs. Pin a package with fetchFromGitHub + callPackage (next chapter) or an overlay of one attr. Fork nixpkgs only when you are actually contributing upstream.

The boring rule

  • Signature final: prev:.
  • Override this package from prev.
  • Refer to other overlay packages via final.
  • Keep overlays small. Files under overlays/.
  • 26.05 follows so the overlay applies to one pkgs.

Try this

  1. Overlay desk-status with writeShellScriptBin; nix-build -E 'with import <nixpkgs> { overlays = [ … ]; }; desk-status' (impure lab) or a flake package.
  2. Two overlays: A sets deskPort = 9;, B builds a string from final.deskPort.
  3. Write the bad final.hello overlay and trigger recursion; switch to prev.
  4. nix flake metadata after adding an overlay-only flake input.