Nix Module Options Explorer
Expert guidance for programmatically querying NixOS and Home Manager module options using flake evaluation.
Core Capability
Ready-made nix eval --impure expressions that evaluate the upstream NixOS and Home Manager option trees from your flake’s locked inputs: option existence, full documentation, and category listings as JSON.
Prerequisites
- nix (with flakes) and jq
- Run from inside a flake repository — expressions use
builtins.getFlake (builtins.toString ./.) - A
nixpkgsflake input (any flake); ahome-managerinput additionally for Home Manager queries - Optional: a flake-parts flake with
debug = true;set in one of its modules (Backend A — overlay-awarepkgs; measured at the same speed as Backend B)
Selecting a Backend
Detect which backend your flake supports:
nix eval --impure --expr '(builtins.getFlake (builtins.toString ./.)) ? currentSystem'true→ Backend A (flake-parts) — reuses your pre-evaluated, overlay-awarepkgsviacurrentSystem.allModuleArgs. Requiresdebug = true;in a flake-parts module. Speed is at parity with Backend B — choose it for overlay-awareness.false→ Backend B (any flake) — instantiatespkgsfromflake.inputs.nixpkgsdirectly. Works in any flake with anixpkgsinput; not overlay-aware.
Both backends expose identical names to queries: lib, hmLib, hmOpts (Home Manager option tree), nixosOpts (NixOS option tree). Nix is lazy — only the option tree your query touches gets evaluated.
Backend A: flake-parts (debug = true)
nix eval --impure --expr '
let
flake = builtins.getFlake (builtins.toString ./.);
args = flake.currentSystem.allModuleArgs;
lib = args.pkgs.lib;
hmLib = lib.extend (self: super: {
hm = import "${flake.inputs.home-manager}/modules/lib" { lib = self; };
});
hmOpts = (hmLib.evalModules {
modules = import "${flake.inputs.home-manager}/modules/modules.nix" {
lib = hmLib;
pkgs = args.pkgs;
check = false;
};
}).options;
nixosOpts = (lib.evalModules {
modules = import "${flake.inputs.nixpkgs}/nixos/modules/module-list.nix" ++ [
{ nixpkgs.hostPlatform = args.system; }
];
}).options;
in
QUERY
'Backend B: any flake (nixpkgs input only)
nix eval --impure --expr '
let
flake = builtins.getFlake (builtins.toString ./.);
system = builtins.currentSystem;
pkgs = import flake.inputs.nixpkgs { inherit system; };
lib = pkgs.lib;
hmLib = lib.extend (self: super: {
hm = import "${flake.inputs.home-manager}/modules/lib" { lib = self; };
});
hmOpts = (hmLib.evalModules {
modules = import "${flake.inputs.home-manager}/modules/modules.nix" {
lib = hmLib;
inherit pkgs;
check = false;
};
}).options;
nixosOpts = (lib.evalModules {
modules = import "${flake.inputs.nixpkgs}/nixos/modules/module-list.nix" ++ [
{ nixpkgs.hostPlatform = system; }
];
}).options;
in
QUERY
'Query Catalog
Substitute any entry for QUERY in the backend expression above. The hmOpts/nixosOpts patterns are interchangeable.
Check if an option exists
Add --apply 'o: o != null' to the nix eval command — it prints true or false directly (no jq needed):
- Home Manager:
hmOpts.programs.duf.enable or null - NixOS:
nixosOpts.services.nginx.enable or null
--apply 'o: o != null' compares the result against null without forcing the option’s contents, so it works on every nixpkgs version (no reliance on value serialization).
Get full option documentation
Pipe to jq for readable output:
lib.optionAttrSetToDocList hmOpts.programs.gitlib.optionAttrSetToDocList nixosOpts.services.nginx.virtualHosts
List all options in a category
Add | jq -r '.[]':
builtins.attrNames hmOpts.programsbuiltins.attrNames nixosOpts.services
Option paths with literal dots
Quote the segment: nixosOpts.services.nginx.virtualHosts."foo.bar". Unquoted, foo.bar parses as nested attributes foo → bar.
Session Mode: Batch Queries in One nix repl
Every nix eval invocation re-pays the module-tree merge (~1s for Home Manager, ~2.5–3.7s for NixOS warm) — the merge dominates, so query shape barely matters. For 2–3 or more queries in one task (the common “does it exist → what options → what defaults” flow), load the backend once in a single nix repl session and issue every query there: measured ~3× faster for 5 queries (2.7s vs 7.4s as separate spawns), ~30ms per additional query after load. nix repl is impure by default, even on machines with pure-eval = true.
Pipe the backend’s bindings as repl assignments — a top-level let … in does NOT define repl bindings and silently discards them:
nix repl <<'EOF'
# Backend B bindings (works in any flake)
f = builtins.getFlake (builtins.toString ./.)
pkgs = import f.inputs.nixpkgs { system = builtins.currentSystem; }
lib = pkgs.lib
hmLib = lib.extend (self: super: {
hm = import "${f.inputs.home-manager}/modules/lib" { lib = self; };
})
hmOpts = (hmLib.evalModules {
modules = import "${f.inputs.home-manager}/modules/modules.nix" {
lib = hmLib; inherit pkgs; check = false;
};
}).options
nixosOpts = (lib.evalModules {
modules = import "${f.inputs.nixpkgs}/nixos/modules/module-list.nix" ++ [
{ nixpkgs.hostPlatform = builtins.currentSystem; }
];
}).options
# Queries — any entry from the Query Catalog, one per line
nixosOpts.services.nginx.enable or null
lib.optionAttrSetToDocList hmOpts.programs.git
builtins.attrNames hmOpts.programs
:q
EOFFor Backend A, replace the first two binding lines with args = f.currentSystem.allModuleArgs; and pkgs = args.pkgs;, and use nixpkgs.hostPlatform = args.system;.
Common Option Categories
Home Manager top-level:
programs.*- User programs (git, vim, zsh, etc.)services.*- User services (gpg-agent, syncthing, etc.)home.*- Home directory settingsxdg.*- XDG directory configurationaccounts.*- Email, calendar accountssystemd.*- User systemd unitsgtk.*,qt.*- Desktop themingwayland.*- Wayland compositors
NixOS top-level:
services.*- System services (nginx, postgresql, etc.)programs.*- System-wide programshardware.*- Hardware configurationboot.*- Boot loader and kernelnetworking.*- Network configurationenvironment.*- System environmentsystemd.*- System unitsusers.*- User managementvirtualisation.*- Containers, VMssecurity.*- Security settings
Option Documentation Fields
lib.optionAttrSetToDocList returns structured documentation:
- name - Full option path (e.g.,
programs.git.enable) - description - What the option does (markdown formatted)
- type - Expected value type (e.g.,
boolean,string,list of string) - default - Default value if not set
- example - Usage examples
- declarations - Source file where option is defined
- visible - Whether option appears in documentation
- readOnly - Whether option can be modified
- internal - Whether option is internal-only
Usage Patterns
- “Does home manager have a duf module?” → existence check on
hmOpts.programs.duf.enable - “What options does
programs.githave?” →lib.optionAttrSetToDocList hmOpts.programs.gitpiped tojq 'map(select(.visible == true)) | map({name, type, description})' - “What programs are available in home manager?” → category listing:
builtins.attrNames hmOpts.programs - “Get the default for
services.nginx.enable” → full docs, read thedefaultfield
Technical Details
Why currentSystem.allModuleArgs (Backend A)?
- Pre-evaluated -
pkgsalready instantiated by flake-parts - System-aware -
args.systemprovides the current system automatically - Overlay-aware - uses your instantiated
pkgs, so your overlays can surface in the rendered defaults ofpackage-type options - Same speed as Backend B - measured at parity; the module-tree merge dominates both backends. Choose Backend A for the overlay-aware
pkgs, not for speed.
Backend B re-instantiates pkgs without your overlays, so package-option defaults reflect plain nixpkgs.
Purity Guarantees
These expressions query upstream modules only:
- Imports directly from
${flake.inputs.home-manager}/modules/modules.nix - Imports directly from
${flake.inputs.nixpkgs}/nixos/modules/module-list.nix - Does NOT include user’s custom modules from
.claude/orhome/ - Does NOT evaluate user’s actual
homeConfigurationsornixosConfigurations
To query user’s actual config (including custom modules):
nix eval --impure --expr '
(builtins.getFlake (builtins.toString ./.))
.homeConfigurations.USERNAME.options.OPTION.PATH
'Best Practices
- Use
jqfor filtering - Pipe documentation output tojqfor readable results - Filter visible options - Use
select(.visible == true)to hide internal options - Check existence first -
--apply 'o: o != null'before pulling full docs - Reference declarations - Use
.declarationsfield to find upstream source - Batch with Session Mode - For 2–3+ queries, one
nix replsession beats repeatednix evalspawns ~3× - Approximate lookups - When the pinned revision doesn’t matter, instant channel-snapshot sources exist (home-manager-options.extranix.com static JSON, search.nixos.org options API) — but they are channel snapshots, not your lockfile
Troubleshooting
error: attribute 'currentSystem' missing
The currentSystem (and allSystems, debug) flake outputs exist only when a flake-parts module sets debug = true;. Either:
- Enable it: add
debug = true;to one of your flake-parts modules and commit (or at leastgit add) the change —builtins.getFlakereads the git tree, so uncommitted edits are invisible. Then use Backend A. - Or skip it: use Backend B, which needs no flake-parts at all.
“path does not exist” error
The flake path must exist. This skill uses builtins.getFlake (builtins.toString ./.) which refers to the current project directory. Ensure you’re running commands from within your flake repository.
“attribute does not exist” error
The option path is wrong. Check:
- Correct category (e.g.,
programsvsservices) - Correct nesting (e.g.,
programs.git.enablenotprograms.git) - Option exists in upstream (not a custom module)
- Segments containing literal dots must be quoted:
nixosOpts.services.nginx.virtualHosts."foo.bar" - Category paths like
nixosOpts.servicesare attribute sets, not options — they trivially “exist”; existence checks are only meaningful for leaf options
Evaluation timing
Each query re-pays the upstream option-tree merge: ~1s for Home Manager, ~2.5–3.7s for NixOS warm on a fast machine, much longer cold while locked inputs are fetched. Query shape barely matters — existence checks, full docs, and category listings cost about the same, because the merge dominates. Separate nix eval spawns re-pay that merge every time: Nix’s eval cache never serves values to nix eval (only leaf attrpath metadata is cached; intermediate results are not). Use Session Mode for multiple queries. For heavy recurring use, materialize the docs once as a derivation (lib.nixosOptionsDoc, Home Manager’s lib.hmOptionsDoc) and nix build it — the build cache does persist built results, unlike the eval cache.
Wrong module system
- Home Manager queries use
hmOpts(HM modules viahmLib.evalModules) - NixOS queries use
nixosOpts(NixOS modules vialib.evalModules)
Related Skills
- nix-flake-parts - the flake-parts framework skill; its Debugging section documents the
debug = true;output andcurrentSystem.allModuleArgsinfrastructure Backend A builds on - nix-dendritic-flake - every-file-is-a-top-level-module flake architecture built on flake-parts