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 the flake root (the directory containing
flake.nix) — expressions usebuiltins.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)
Selecting a Backend
Detect which backend your flake supports:
nix eval --impure --json --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.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 --json --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 {
class = "homeManager";
specialArgs.modulesPath = "${flake.inputs.home-manager}/modules";
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; nixpkgs.pkgs = args.pkgs; }
];
}).options;
in
QUERY
'Backend B: any flake (nixpkgs input only)
nix eval --impure --json --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 {
class = "homeManager";
specialArgs.modulesPath = "${flake.inputs.home-manager}/modules";
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; nixpkgs.pkgs = pkgs; }
];
}).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
Both backend commands include --json; keep it when piping to jq. Without it, nix eval prints Nix syntax, which jq cannot parse.
Pipe documentation results 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
Submodule options and literal dots
virtualHosts is an option containing an attribute set of submodules, not an option subtree indexed by configured host names. Get its nested declarations with lib.optionAttrSetToDocList nixosOpts.services.nginx.virtualHosts; docs use placeholders such as <name>. To inspect its suboption tree directly:
(nixosOpts.services.nginx.virtualHosts.type.getSubOptions
[ "services" "nginx" "virtualHosts" ]).enableACMEFor configured values, use the actual configuration output, for example flake.nixosConfigurations.HOST.config.services.nginx.virtualHosts."foo.bar". Quote attribute names containing literal dots; unquoted foo.bar selects nested attributes.
Session Mode: Batch Queries in One nix repl
For several related queries, reuse one nix repl session to avoid repeatedly evaluating the module tree. Actual savings depend on the pinned inputs, cache state, and machine.
Pipe bindings as REPL assignments, with each complete assignment on one physical line. Multiline assignments in piped input can be parsed as separate commands. A top-level let … in evaluates an expression without retaining its local bindings:
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 { class = "homeManager"; specialArgs.modulesPath = "${f.inputs.home-manager}/modules"; 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; nixpkgs.pkgs = pkgs; } ]; }).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 f = builtins.getFlake (builtins.toString ./.), args = f.currentSystem.allModuleArgs, and pkgs = args.pkgs (one assignment per line), 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. default and example are optional and may be rendered literal-expression records rather than raw values:
- 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
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 --json --apply 'o: { inherit (o) description; type = o.type.description; }' --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 - Reuse bindings for several related queries
- 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;.
Prefer Backend B if debug mode is absent. If enabling it is within the requested scope, add debug = true; to a flake-parts module. Local Git flakes include edits to tracked files without a commit; newly created files must be tracked (for example, git add path/to/module.nix). Do not stage unrelated files just to query options.
“path does not exist” error
The flake path must exist. This skill uses builtins.getFlake (builtins.toString ./.) which refers to the current project directory. Run from the directory containing the intended flake.nix.
“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)
- Dynamic submodule names are not attributes in the upstream option tree; use
type.getSubOptionsor the documentation list, as shown above - Category paths like
nixosOpts.servicesare attribute sets, not options — they trivially “exist”; existence checks are only meaningful for leaf options
Documentation forces missing configuration
Existence checks and category listings can succeed while full documentation fails: serializing a default or example may force a required username, home directory, platform-specific package, or other configuration. Narrow the subtree or project only the needed fields inside the Nix expression before JSON serialization:
map (o: { inherit (o) name type description declarations; })
(lib.optionAttrSetToDocList hmOpts.programs.git)Filtering with jq happens after serialization and cannot prevent such evaluation errors. If values are still required, report the missing context rather than inventing user configuration or changing the lockfile.
Evaluation timing
Fetching locked inputs and evaluating upstream modules can take time. Reuse Session Mode for related queries rather than relying on a fixed timing estimate. For recurring documentation needs, use the pinned upstream documentation builders and cache their build results; inspect their interfaces at that revision before invoking them.
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