You already know how to install packages. This is how you reshape them.
Here's a scenario every Nix user eventually runs into: you declare a package, it builds, everything's fine — until you need it to behave slightly differently. Maybe it pulls in a dependency you don't want. Maybe you need an older version because the new one broke something. Maybe there's a one-line patch sitting in a GitHub issue that upstream hasn't merged yet.
On a traditional distro, this usually means forking the package: grab the build script, hand-edit it, and now you own a copy forever, drifting further from upstream every release. On Nix, you don't fork — you override. You take the existing package definition and layer a change on top of it with a function. No fork, no copy-paste, no permanent maintenance burden.
This post walks through the three tools Nix gives you for this: override, overrideAttrs, and overlays. By the end you'll know which one to reach for, how they actually work under the hood, and how to dodge the infinite-recursion trap that catches basically everyone the first time they write an overlay.
Why Overrides Exist
Here's the thing that makes this all possible: Nix packages aren't static blobs, they're functions. A package like graphviz takes its dependencies as function arguments and hands back a derivation:
In graphviz.nix (simplified — the real one has more arguments):
{ mkDerivation, gd, fontconfig, libjpeg, bzip2 }:
mkDerivation { /* ... */ }
When you reference pkgs.graphviz, nixpkgs has already called that function with a default set of arguments and handed you the result. Nice and simple — until you need something different.
Say you want graphviz built against a custom gd. Without overrides, your only option is to re-import the whole thing yourself:
# The manual way — works, but now you're maintaining a fork
mygraphviz = import ./graphviz.nix {
inherit mkDerivation fontconfig libjpeg bzip2;
gd = customgd;
};
That compiles, sure, but you've just copy-pasted an entire package definition to change one line. Any security fix or build improvement upstream ships from here on out just... doesn't reach you, because you're no longer pointing at their code.
override fixes this by letting you keep the original derivation and apply a function on top of it:
mygraphviz = pkgs.graphviz.override { gd = customgd; };
That's the whole thing. One line. The original package definition stays untouched, your change rides on top of it, and when nixpkgs updates graphviz next month, you get the update automatically with your customization still applied.
The Three Override Mechanisms
Nix gives you three tools for this, and they operate at different layers:
| Mechanism | What it changes | Scope | When to use |
|---|---|---|---|
override |
The arguments passed into the package function | Package inputs | Swap a dependency, toggle a feature flag |
overrideAttrs |
The attribute set passed to mkDerivation |
The build recipe itself | Change version, source, patches, build options |
overrideDerivation |
Raw derivation attributes, post-processing | Low-level drv output | Rarely — prefer overrideAttrs |
If you only remember one rule from this whole post, make it this one: try override first. If the package is built with stdenv.mkDerivation — and the vast majority are — reach for overrideAttrs when you need to touch the build itself. Leave overrideDerivation alone; it operates below mkDerivation, after most of the useful processing has already happened, which makes it fragile and easy to break in confusing ways.
override: Changing What Goes Into the Package
override re-calls the package function with different arguments. The package itself doesn't change — you're just swapping what feeds into it.
How It Works
Every package that goes through callPackage — which, again, is most of nixpkgs — automatically gets an .override attribute attached to it. There's no magic here; it's a small pattern called makeOverridable:
# Conceptual — this is roughly what makeOverridable does
makeOverridable = f: origArgs:
let origRes = f origArgs;
in origRes // { override = newArgs: f (origArgs // newArgs); };
In plain English: it remembers the original arguments, and .override re-runs the function with your new arguments merged on top via //.
pkgs.graphviz.override { gd = customgd; }
You can swap more than one argument at a time, too:
pkgs.graphviz.override {
gd = customgd;
fontconfig = customFontconfig;
}
Finding What You're Allowed to Override
The obvious question: how do you know what arguments a given package actually accepts? Two ways.
Read the source directly:
nix edit -f '<nixpkgs>' graphviz
This opens the package's .nix file in your editor. The function's arguments are your override menu — whatever shows up in that first { ... }: block is fair game.
Or ask nix repl:
nix repl -f '<nixpkgs>'
nix-repl> pkgs.graphviz.override.__functionArgs
{ bzip2 = false; fontconfig = false; gd = false; libjpeg = false; ... }
false means the argument has a default, so it's optional to override. true means it's required — the package won't build without it.
Real Example: Building a Custom PHP with Extra Extensions
Graphviz is the textbook example, but here's one that actually comes up in the wild. If you self-host anything PHP-based (Nextcloud, a legacy app, whatever), you'll eventually need extensions the default php package doesn't ship with:
In configuration.nix or a flake module:
pkgs.php.withExtensions ({ enabled, all }: enabled ++ [
all.redis
all.imagick
])
Under the hood, withExtensions is just override wearing a friendlier name — it's calling the package function with a different set of extensions passed in. Same mechanism, nicer interface.
Nesting Overrides
You can override an argument that is itself an overridden package:
pkgs.graphviz.override {
gd = pkgs.gd.override { fontconfig = pkgs.fontconfig.dev; };
}
Read that from the inside out: first gd gets overridden to use fontconfig.dev, and that custom gd becomes the input to the graphviz override. It composes cleanly because each .override call is just a function call — nothing special happening besides that.
Chaining Overrides
Because .override returns a derivation that still has its own .override attached, you can chain calls:
pkgs.graphviz
.override { gd = customgd; }
.override { fontconfig = customFontconfig; }
Each link in the chain re-invokes the package function with the cumulative set of argument changes so far.
overrideAttrs: Changing the Build Itself
Where override changes what goes into the package function, overrideAttrs changes what goes into mkDerivation — the actual build recipe.
Reach for this one when you need to:
- Bump the version or swap the source
- Add a patch
- Flip a build-time switch (
doCheck,separateDebugInfo, and so on) - Rename the package output
How It Works
overrideAttrs takes a function from the old attribute set to a new one:
pkgs.hello.overrideAttrs (oldAttrs: {
doCheck = false;
})
Whatever you return gets merged on top of the original attributes. Everything you don't mention stays exactly as it was — you're not rebuilding the whole recipe, just patching one line of it.
The Modern Syntax: finalAttrs and previousAttrs
Newer nixpkgs packages use a fixed-point style where you can reference your own overrides while defining them:
pkgs.hello.overrideAttrs (finalAttrs: previousAttrs: {
version = "2.12.1";
src = pkgs.fetchurl {
url = "mirror://gnu/hello/hello-${finalAttrs.version}.tar.gz";
sha256 = "...";
};
})
previousAttrs is what the package looked like before your change. finalAttrs is what it's going to look like after — which is why you can write finalAttrs.version inside the src URL and have it resolve correctly, even though version is defined in that same block. If all you're doing is appending a patch, the older single-argument form still works fine and is arguably easier to read:
pkgs.hello.overrideAttrs (old: {
patches = (old.patches or []) ++ [ ./fix.patch ];
})
Real Examples
Bumping a version and pointing at a new source:
pkgs.sl.overrideAttrs (old: {
version = "custom-2024";
src = pkgs.fetchFromGitHub {
owner = "mtoyoda";
repo = "sl";
rev = "923e7d7ebc5c1f009755bdeb789ac25658ccce03";
hash = "sha256-173gxk0ymiw94glyjzjizp8bv8g72gwkjhacigd1an09jshdrjb4";
};
})
Adding a patch on top of the existing ones (instead of replacing them):
pkgs.mutter.overrideAttrs (old: {
patches = (old.patches or []) ++ [
(pkgs.fetchpatch {
url = "https://example.com/fix.patch";
hash = "sha256-...";
})
];
})
Note the old.patches or [] — that's a defensive habit worth keeping. If the package has no patches attribute at all, old.patches would error out; or [] just falls back to an empty list instead.
Skipping a slow test suite for a package you only need locally:
pkgs.openssl.overrideAttrs (old: {
doCheck = false;
})
Handy for a quick dev shell where you don't want to wait on OpenSSL's full test suite every rebuild — just don't ship that in production.
Toggling a stdenv-level feature:
pkgs.hello.overrideAttrs (old: {
separateDebugInfo = true;
})
separateDebugInfo isn't something hello defines itself — it's an option stdenv.mkDerivation understands and acts on. overrideAttrs can set it because it intercepts the attribute set before mkDerivation processes it.
Overlays: Customizing the Whole Package Set at Once
override and overrideAttrs operate one package at a time. Overlays step up a level: they let you reshape the entire nixpkgs package set, systematically and reproducibly.
An overlay is just a function of two arguments that returns an attribute set of changes:
final: prev: {
# your changes go here
}
previs nixpkgs as it looked before this overlay ranfinalis nixpkgs as it looks after every overlay has been applied
Following the Data Flow
Picture overlays as stacked layers. Each layer can see two things: everything below it (prev), and the fully-merged final result (final):
nixpkgs (base)
↓
overlay 1: prev = base, final = base + overlay 1
↓
overlay 2: prev = base + ol1, final = base + ol1 + ol2
↓
final package set
The rule that trips everyone up at least once: when you're defining an attribute, reference prev to get the original version of something, and final only when you want to reference something you (or a later overlay) defined:
final: prev: {
firefox = prev.firefox.override { /* ... */ }; # ✓ correct — builds on the original
myBrowser = final.firefox; # ✓ correct — points at your own definition
}
Real Example: A Small "Homelab Tools" Overlay
Here's a shape you'll actually reach for on a self-hosted setup — bundling a few small customizations into one overlay you can drop into any machine's config:
In overlays/homelab.nix:
final: prev: {
# Pin restic to a specific version because a newer release broke a backup script
restic = prev.restic.overrideAttrs (old: rec {
version = "0.16.4";
src = prev.fetchFromGitHub {
owner = "restic";
repo = "restic";
rev = "v${version}";
hash = "sha256-...";
};
});
# A wrapper script that isn't in nixpkgs at all — no "prev" to build on
backup-check = prev.writeShellScriptBin "backup-check" ''
${final.restic}/bin/restic snapshots --json | ${final.jq}/bin/jq '.[0].time'
'';
}
Two things worth noticing: restic overrides an existing package using prev as its base, while backup-check is a brand-new package that didn't exist before — it just uses final to reference restic and jq so it always gets the fully-overlaid versions of both.
The Infinite Recursion Trap
# ✗ Broken — infinite recursion
final: prev: {
hello = final.hello.override { /* ... */ };
}
# ✓ Fixed
final: prev: {
hello = prev.hello.override { /* ... */ };
}
Why does the first one blow up? final.hello is the attribute you're in the middle of defining. Referencing it while defining it means Nix has to evaluate hello to figure out what hello is — a loop with no base case. prev.hello breaks the cycle because it points at the package as it existed before this overlay touched it.
Applying Overlays
In a flake:
# flake.nix
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux";
myOverlay = final: prev: {
hello = prev.hello.overrideAttrs (old: {
pname = "my-hello";
});
};
pkgs = import nixpkgs {
inherit system;
overlays = [ myOverlay ];
};
in {
packages.${system}.default = pkgs.hello;
};
}
In NixOS:
# configuration.nix
{ pkgs, ... }:
{
nixpkgs.overlays = [
(final: prev: {
hello = prev.hello.overrideAttrs (old: {
pname = "my-hello";
});
})
];
}
In a shell.nix for a one-off project:
{ pkgs ? import <nixpkgs> {
overlays = [
(final: prev: {
hello = prev.hello.overrideAttrs (old: { pname = "my-hello"; });
})
];
}
}:
pkgs.hello
Auto-loaded, user-level:
Drop overlays in ~/.config/nixpkgs/overlays.nix, or as individual files under ~/.config/nixpkgs/overlays/. Every Nix tool you run as your user picks these up automatically — no nixos-rebuild required. The tradeoff is that these have zero effect on your actual NixOS system configuration, only on ad-hoc nix-shell / nix build invocations.
Stacking Multiple Overlays
Composing overlays is the entire point of the feature:
nixpkgs.overlays = [
overlay1
overlay2
overlay3
];
They apply left to right. Each overlay's prev includes everything the overlays before it did. And here's the subtle part: the final argument is identical across all of them — it's always the complete, fully-merged result, regardless of where in the list a given overlay sits.
Practical Recipes
Recipe 1: Patching a Package Upstream Hasn't Fixed Yet
You hit a bug, there's a fix sitting in a GitHub PR, and you don't want to wait for the next nixpkgs release:
In an overlay:
final: prev: {
myApp = prev.myApp.overrideAttrs (old: {
patches = (old.patches or []) ++ [
(prev.fetchpatch {
url = "https://github.com/owner/repo/commit/abc123.patch";
hash = "sha256-...";
})
];
});
}
Recipe 2: Toggling a Build Feature
Plenty of packages expose boolean flags for optional features. pass, for instance, can drop its X11 clipboard integration entirely:
final: prev: {
pass = prev.pass.override { x11Support = false; };
}
Check what flags a package actually exposes the same way as before — nix edit -f '<nixpkgs>' pass, or :e pass inside nix repl.
Recipe 3: Overriding Inside Scopes (overrideScope)
Some packages don't live at the top level of pkgs — they sit inside a scope, an attribute set like python3Packages, perlPackages, or gnome. You can't override those directly; you need overrideScope:
final: prev: {
gnome = prev.gnome.overrideScope (gfinal: gprev: {
mutter = gprev.mutter.overrideAttrs (old: {
patches = (old.patches or []) ++ [
(prev.fetchpatch {
url = "https://example.com/mutter-fix.patch";
hash = "sha256-...";
})
];
});
});
}
Same pattern as a top-level overlay, just nested one level deeper: gprev is the scope before your change, gfinal is the scope after. Build your change on gprev, reference your own additions through gfinal.
Same idea for Neovim plugins, which is probably the most common reason a homelab tinkerer ends up here:
final: prev: {
vimPlugins = prev.vimPlugins.extend (final': prev': {
my-plugin = prev'.callPackage ./packages/my-plugin { };
});
}
Recipe 4: Adding a Package to a Python Environment
Python packages live inside python.pkgs, which means they need their own nested overlay via packageOverrides:
final: prev: {
python = prev.python.override {
packageOverrides = pyfinal: pyprev: {
my-lib = pyprev.buildPythonPackage {
pname = "my-lib";
version = "1.0.0";
src = pyprev.fetchPypi {
pname = "my-lib";
version = "1.0.0";
hash = "sha256-...";
};
propagatedBuildInputs = [ pyprev.requests ];
};
};
};
# Expose it at the top level so it's easy to reach
pythonPackages = final.python.pkgs;
}
packageOverrides is essentially an overlay-inside-an-overlay, following the exact same final/prev naming convention — pyfinal is your own definitions, pyprev is the set you're building on.
From a shell, you'd reach it like this:
nix-shell -p pythonPackages.my-lib
Recipe 5: A Quick Version Bump Without Touching the Build
If all you need is a newer commit of something, overrideAttrs alone gets you there without a full overlay-scope dance:
final: prev: {
myTool = prev.myTool.overrideAttrs (old: rec {
version = "2.1.0";
src = prev.fetchFromGitHub {
owner = "owner";
repo = "my-tool";
rev = "v${version}";
hash = "sha256-...";
};
});
}
The rec keyword is what lets src reference version from the same attribute set.
Recipe 6: A Flake That Applies an Overlay to a Pinned Nixpkgs
Putting it all together — a minimal, working flake:
{
description = "My system with overlays";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixpkgs-unfree.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux";
myOverlay = final: prev: {
hello = prev.hello.overrideAttrs (old: {
pname = "greeted-hello";
});
};
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [ myOverlay ];
};
in {
packages.${system}.default = pkgs.hello;
};
}
Override vs. Overlays: Which One Do You Actually Reach For?
| Scenario | Use | Why |
|---|---|---|
| A one-off tweak inside a single project | override or overrideAttrs |
Scoped to that expression only |
| A system-wide change on your NixOS box | An overlay in nixpkgs.overlays |
Applies everywhere pkgs is used |
| Sharing a customization with other people | An overlay file | Composable and distributable |
| Flipping a feature flag | override |
Direct argument swap |
| Patching source or adding patches | overrideAttrs |
It's a build-level change |
Reaching inside gnome, python, etc. |
overrideScope |
Scope-level overlay |
| Several related changes across packages | An overlay | One place, one consistent story |
The short version: if you're writing it once inside a let block, use override / overrideAttrs. If you want it to apply everywhere pkgs shows up on that machine (or on every machine that imports your overlay), write an overlay.
Common Pitfalls
Infinite Recursion (again, because it's the one everyone hits)
Covered above, but worth repeating on its own: inside an overlay, final.pkg is the thing you're defining — reference prev.pkg for the original instead, or Nix will loop trying to evaluate itself.
Rust Packages Need cargoDeps Overridden Too
Rust packages built with buildRustPackage don't let you just bump cargoHash and call it done — the vendored dependency archive (cargoDeps) needs its own override, or the build will fail with a hash mismatch that has nothing to do with the source hash you actually changed:
final: prev: {
rnix-lsp = prev.rnix-lsp.overrideAttrs (old: rec {
version = "master";
src = prev.fetchFromGitHub {
owner = "nix-community";
repo = "rnix-lsp";
rev = "...";
hash = "sha256-...";
};
cargoDeps = old.cargoDeps.overrideAttrs (prev.lib.const {
name = "rnix-lsp-vendor.tar.gz";
inherit src;
outputHash = "sha256-...";
});
});
}
(If you're following along in an older blog post or Stack Overflow answer and see oldAttrs.cargoDeps instead of old.cargoDeps — check the function signature above it. The variable name has to match whatever the overrideAttrs function actually names its argument, which trips people up more often than you'd expect.)
This is a known rough edge in how Rust packaging works in nixpkgs — see nixpkgs#107070 for the ongoing discussion.
"override Isn't Doing Anything"
If .override seems to silently do nothing, check whether the package is actually built through callPackage in the first place:
nix edit -f '<nixpkgs>' package-name
If what you find isn't a function — just a plain attribute set — there's nothing for override to re-call. Reach for overrideAttrs instead.
"My Overlay Isn't Applying"
This one catches a lot of people switching from nix-env habits: overlays set through nixpkgs.overlays in configuration.nix only affect your NixOS system configuration — the thing nixos-rebuild evaluates. They have no effect on ad-hoc nix-shell, nix build, or other CLI invocations you run yourself, because those import nixpkgs independently. If you want an overlay to follow you everywhere, apply it at import time instead — in your flake.nix, your shell.nix, or your user-level ~/.config/nixpkgs/overlays.nix.
Quick Reference
| Mechanism | Syntax | What it does |
|---|---|---|
override |
pkg.override { arg = val; } |
Re-calls the package function with new arguments |
overrideAttrs |
pkg.overrideAttrs (old: { ... }) |
Changes the attributes passed to mkDerivation |
overrideDerivation |
pkg.overrideDerivation (old: { ... }) |
Changes raw derivation attributes (avoid this) |
overrideScope |
scope.overrideScope (gfinal: gprev: { ... }) |
Overrides packages living inside a scope |
| Overlay | final: prev: { ... } |
Systematic customization of the whole package set |
| Apply overlay (flake) | import nixpkgs { overlays = [ ... ]; } |
Injects overlays at import time |
| Apply overlay (NixOS) | nixpkgs.overlays = [ ... ] |
System-wide overlay, nixos-rebuild only |
| Auto-load overlay | ~/.config/nixpkgs/overlays.nix |
User-level overlay, applied to every Nix tool you run |
The Bottom Line
override, overrideAttrs, and overlays are the same underlying idea at three different scales. override reshapes one package's inputs. overrideAttrs reshapes one package's build. Overlays reshape the entire package set — consistently, and in a way you can actually share with someone else's machine.
None of it requires forking nixpkgs. You're layering changes on top of a moving target, and because the layer is a function rather than a copy, it keeps working when upstream moves. Share the overlay file with a teammate and they get the exact same customizations without losing any of their own.
That's the practical payoff of derivations being values and packages being functions: everything composes. You already knew how to install packages. Now you know how to reshape them — and that's most of the difference between using Nix and actually owning your system configuration.