Rules
Rules live in nest.rules, as a list or as an attrset keyed by selector or label
(see Rule forms). Each rule has:
is— selector determining which nodes this rule applies to- One or more class keys (
nixos,user, etc.) — config contributed to matching nodes
nest.rules = [ { is = nest.host; # selector nixos = { … }; # class key: contributes to nixos class config }];Rule forms
Section titled “Rule forms”nest.rules accepts three shapes, all normalised to the same internal list:
# 1. List form — an explicit `is` per rule:nest.rules = [ { is = nest.host; nixos = { }; } ];
# 2. Selector-keyed — the attrset KEY is the selector string:nest.rules."host > user".user = { };
# 3. Label-keyed — the key is just a label, the selector lives in `is`# (handy when the selector is a DSL value rather than a string):nest.rules.hostDefaults = { is = nest.host; nixos = { }; };The is selector
Section titled “The is selector”Any selector form:
is = nest.hostis = [ nest.host nest.web ]is = [ nest.host (nest.has nest.admin) ]is = nest.attrs { env = "prod"; }is = "*.web"Class keys
Section titled “Class keys”Each key besides is is a class key. It contributes config to the matching node’s merged config for that class.
Static attrset
Section titled “Static attrset”{ is = nest.nginx; nixos.services.nginx.enable = true;}Function (receives select + entity arg)
Section titled “Function (receives select + entity arg)”When the value is a function, it receives named args it declares:
{ is = nest.host; nixos = { host, select, ... }: { networking.hostName = host.name; networking.extraHosts = lib.concatMapStringsSep "\n" (p: "${p.addr} ${p.name}") (select.siblings nest.host); };}Available args:
| Arg | Type | Description |
|---|---|---|
select | select context | Select API for this node |
node | node | The matched node, by a fixed name |
| entity arg | node | The node itself, keyed by entity trait name (host, user, alice, etc.) |
Only declare the args you need — unused args are filtered out.
Deferred arguments
Section titled “Deferred arguments”An argument nest cannot resolve from the node’s context — such as pkgs, lib,
or config — is deferred: nest hands the function to the NixOS module
system, which supplies those arguments at evaluation time. So a rule can ask for
pkgs and use it exactly like a normal module:
{ is = nest.host; nixos = { pkgs, ... }: { environment.systemPackages = [ pkgs.git ]; };}Function arg for different entity types
Section titled “Function arg for different entity types”# host entitynixos = { host, ... }: { networking.hostName = host.name; }
# user entityuser = { user, ... }: { isNormalUser = true; openssh.authorizedKeys.keys = user.sshKeys; }
# any node — just use selectnixos = { select, ... }: { result = select.node.someAttr; }Multiple rules, same node
Section titled “Multiple rules, same node”When multiple rules match a node, their contributions are collected as a list — one entry per matching rule. Nest does not deep-merge them. The class fn receives the full list and passes it to nixosSystem, which lets the NixOS module system handle merging with proper semantics (lib.mkForce, lib.mkDefault, type checking, conflict detection):
nest.rules = [ { is = nest.host; nixos.networking.hostName = "default"; } { is = "#web-1"; nixos.networking.hostName = "web-1"; } # Both entries go into the modules list passed to nixosSystem. # NixOS sees two definitions for networking.hostName and merges by option type.];Multiple rules targeting different keys compose naturally:
{ is = nest.host; nixos.nix.settings.experimental-features = [ "nix-command" ]; }{ is = nest.nginx; nixos.services.nginx.enable = true; }
# For a node with both host and nginx:# nixosSystem receives both attrsets as separate modules — no nest-level merge neededThe synth key
Section titled “The synth key”A rule can carry a synth key — processed identically to trait synth but fired by rule matching:
{ is = [ nest.host (nest.attrs { env = "prod"; }) ]; synth = { select, ... }: { node.children = map (u: { inherit (u) name sshKeys; is = [ nest.user nest.admin ]; }) (select nest.admin); };}Rule synth runs after all rule matching completes. Synthesized children are then re-evaluated against all rules — they participate fully in output processing.
Rule ordering
Section titled “Rule ordering”Rules are evaluated in list order. Within a single rule, class keys are merged in attrset key order (Nix attrset key order is alphabetical). Across multiple rules, later rules override earlier ones for the same path.
Complete rule example
Section titled “Complete rule example”nest.rules = [
# Static config for all hosts { is = nest.host; nixos = { system.stateVersion = "25.11"; boot.loader.grub.enable = false; }; }
# Dynamic config using select.node { is = nest.host; nixos = { host, ... }: { networking.hostName = host.name; }; }
# Cross-node query via select.siblings { is = nest.lb; nixos = { select, ... }: let webs = select.siblings nest.web; in { services.haproxy.backends = map (w: w.addr) webs; }; }
# Compound selector: host with admin child { is = [ nest.host (nest.has nest.admin) ]; nixos.security.sudo.wheelNeedsPassword = false; }
# Rule synth: inject children { is = [ nest.host (nest.attrs { env = "prod"; }) ]; synth = { select, ... }: { node.children = map (u: { inherit (u) name sshKeys; is = [ nest.user nest.admin ]; }) (select nest.admin); }; }
# User rule: fires on synthesized user children { is = nest.user; user = { user, ... }: { isNormalUser = true; openssh.authorizedKeys.keys = user.sshKeys; }; }
# Admin role adds wheel group { is = nest.admin; user.extraGroups = [ "wheel" ]; }
];