I kept hitting the same wall debugging my configs: which module actually set this option, why didn’t my definition win, why won’t it set at all? It comes up here a fair bit (how do you even inspect a config?, conflicting definitions). nixos-option and nix repl each answer a slice, but never the whole “who set what, at what priority” story, so I wrote a little tool for it.
There’s also why-not (why an option isn’t set, including definitions sitting behind an mkIf that didn’t fire), what-sets, search, and small siblings for merge conflicts, overlay attribution, and reading an infinite-recursion trace. Works on NixOS, home-manager, nix-darwin and flake-parts, with JSON output if you want to script it.
Honest about the edges: the default view (value, priority, type, declaration) works on any config. The deeper per-definition view (--full) is best-effort and degrades on big imported configs. That’s a module-system limitation more than a nix-why one, and arguably the bit worth fixing upstream so any tool could use it.
It’s not a searcher or value browser like Optnix or nix-inspect, more the “explain the merge” counterpart to those.
Two things I’d love to hear: is this actually useful to you, and does the provenance look right when you point it at your own config? And if you’ve ever wanted the module system to expose this natively, say so, it’d help me decide whether an RFC is worth writing.
Definitely a cool and useful introspection idea! If it delivers on the promise, I would certainly be happy to have a better nixos-option.
Looking at the README.md, it seems like the CLI interface is nix-why-option <host> <subcommand>. The <host> part isn’t documented at all on that page, although it looks like a flake reference for a nixosConfiguration.
Since it isn’t immediately clear, is this usable with configuration.nix or the new system.nix?
For mergable options (like environemnt.systemPackages), does it say which lines/elements were added from which file?
Thanks, good questions, and you’ve found a genuine doc gap.
Argument order. It’s nix-why-option [subcommand] <target> <option-path>, so the target comes first and the subcommand (if any) before it. <target> is a flake reference to a configuration: .#nixosConfigurations.krach, the shorthand .#krach, or <path>#<attr> / github:owner/repo#<attr>. You’re right that the README only shows it in examples and never says what <target> actually is. I’ll fix that.
Non-flake configuration.nix. Yes, it works, it just isn’t flake-only underneath. The eval subcommand takes any expression that evaluates to a config, and import <nixpkgs/nixos> { configuration = ...; } is the non-flake entry point:
You currently need --adapter nixos because autodetect doesn’t yet recognise that raw shape; I’ll make it automatic so the flag isn’t needed.
Mergeable options. Yes. what-sets environment.systemPackages lists every file that contributes and the exact elements each one added, so you get file → elements rather than just “these files touched it”. On one of my hosts that’s ~95 files, each with its own slice of the list; --full adds the line within the file. It won’t dedup the same element added by two files, but it shows you both.
Appreciate the feedback. The README fix and the autodetect smoothing are going in.
Fair question. Yeah, a good chunk of it was written with an LLM (Claude), with me steering. I picked the approach (read the evaluated options tree by default so it can’t crash, opt into the raw module-walk for the deeper per-definition stuff, and be upfront about where that degrades), reviewed the changes, and put most of the effort into testing rather than trusting it blindly. It’s been run against my own machines plus a handful of public configs (Mic92, Misterio77, ryan4yin, fufexan, NotAShelf/nyx, fortuneteller2k), with the resolved values and priorities cross-checked against nix eval, and there’s a test suite in the repo.
So: assistant-written, but reviewed and tested, not fire-and-forget. The design calls and the honest-limitations notes are mine, and I’m happy to get into any of them. I also get not wanting to run code you can’t vouch for. It’s a small thing, a few bash scripts over a pure-Nix lib, so it reads quickly, and the --json output means you don’t have to trust the renderer to use the data.
Great question, and that’s exactly the direction I’d like to take it.
Some of it is already native: options.<path>.definitionsWithLocations gives you { file, value } per definition in the repl today, and nix-why’s default output is basically a friendlier wrapper over that.
What the module system discards during the merge, and what a tool currently has to reconstruct, is the richer provenance: the priority kind per definition (mkDefault / mkForce / …), which definitions got filtered out by a false mkIf (and the condition they were waiting on), and per-definition line numbers. Exposing those as option metadata, so the repl or any tool could read them without re-walking the module list, is the part that would need an RFC. nix-why is partly me scratching that itch and partly a proof of need for it.
I’ve been meaning to write that up properly. If you’d be up for sanity-checking the shape before it goes to the module-system folks, I’d genuinely value that.