nix-config
This skill provides comprehensive guidance for working with the nix-config framework - a platform-agnostic modular configuration system for NixOS and Darwin (macOS). Use this skill when adding new mod
Files6
View on GitHubSKILL.mdclaude/skills/nix-config
GitHubnix-config
Operating manual for ${PWD}/nix-src/nix-config (public logic) and ${PWD}/nix-src/nix-secrets (private data, separate flake input). The repo manages 7 NixOS hosts (plus the iso installer) and 1 Darwin host through a unified flake. This skill encodes the conventions so a new session can add or modify a module without re-reading the whole tree.
Primary use case: adding or extending home / host modules. The decision tree below routes you to the right pattern; everything else (hosts, packages, overlays, secrets) is built around the same data bus.
Core mental model
The repo has three composition lanes, all wired through one data bus.
Lane 1 — Hosts. Each host lives at hosts/{nixos,darwin}/<HostName>/default.nix and is a thin file: it sets hostSpec.hostName, declares networking, then cherry-picks modules from hosts/common/{core,optional,users}. hosts/common/core is mandatory and provides hostSpec, sops, ssh, home-manager wiring. hosts/common/optional/** is opt-in — nothing under optional/ is auto-imported; the host config explicitly lists each file via lib.custom.relativeToRoot. hosts/common/users/<u> adds a user to the host.
Lane 2 — Home. Each (user, host) pair lives at home/<user>/<HostName>.nix and imports common/core plus selected common/optional/<cat>. home/<user>/common/core/default.nix picks the platform variant via ./${platform}.nix where platform = if hostSpec.isDarwin then "darwin" else "nixos" — a normal Nix import, not magic.
Lane 3 — Modules. Reusable options-providing modules live under modules/hosts/{common,nixos,darwin}/<name>/ and modules/home/. These are auto-imported by lib.custom.scanPaths from the corresponding default.nix (e.g. modules/hosts/nixos/default.nix scans its directory). Dropping a .nix file (or directory containing default.nix) here wires it in. The module exposes options like <name>.enable = true; the host turns it on by setting that option, not by importing the file.
The data bus. modules/common/host-spec.nix defines the hostSpec option tree (see references/hostspec.md). hosts/common/core/default.nix populates it once with inherit (inputs.nix-secrets) networkInfo serviceInfo networkStorageInfo networking domain email userFullName;. After that, every host, module, and home-manager file reads config.hostSpec.networkInfo.hosts.${hostSpec.hostName}, config.hostSpec.serviceInfo.<svc>, etc. Module code should never reach inputs.nix-secrets directly — that indirection is why secrets and host data can be swapped, audited, or stubbed in one place.
Platform separation — two distinct uses of default.nix + nixos.nix + darwin.nix. The pattern shows up in two places and works differently in each:
- In
modules/hosts/{common,nixos,darwin}/, the directory itself is platform-scoped —modules/hosts/nixos/foo.nixis only loaded on NixOS hosts becausemodules/hosts/nixos/default.nixis only imported by NixOS host configs. - In
home/<u>/common/core/,hosts/common/core/, andhosts/common/users/<u>/, all three files coexist anddefault.nixchooses which sibling to import based onhostSpec.isDarwin. The naming is convention; the wiring is the explicit./${platform}.nixline.
Quick recipes
Recipe numbers below match references/recipes.md 1-to-1. Decision tree — pick the first match:
- New system feature, only used by some hosts, no options needed → Recipe 1 (drop-in optional service).
- New system feature with configurable options, opt-in via
<name>.enable→ Recipe 2 (reusable module). - New custom package (not in nixpkgs) → Recipe 3.
- Override a nixpkgs package → Recipe 4.
- New home-manager dotfile or program → Recipe 5.
- New desktop / compositor (system + home halves) →
references/recipes.mdRecipe 6 "Add a desktop (system + user halves)". - New host →
references/recipes.mdRecipe 7 "Add a new host (brief)". - Service that consumes a sops secret →
references/recipes.mdRecipe 8 "Wire a service that needs a secret". - New user → no dedicated recipe; mirror an existing user. See "Adding a user" below.
Recipe 1 — drop-in optional system service
Create hosts/common/optional/services/<name>.nix with a config-only module:
{ config, lib, pkgs, ... }:
{
services.<name>.enable = true;
# ... whatever else this service needs
}
Then in each host that wants it, add the path to the imports list in hosts/<platform>/<HostName>/default.nix:
imports = lib.flatten [
./hardware-configuration.nix
(map lib.custom.relativeToRoot [
"hosts/common/core"
"hosts/common/optional/services/<name>.nix"
])
];
Why: hosts/common/optional/ is deliberately not auto-scanned. The host config is the single audit point that tells you what a machine actually runs. Real examples: hosts/common/optional/services/docker.nix, attic.nix, tailscale.nix. Full template: references/recipes.md Recipe 1.
Recipe 2 — reusable module with options (preferred for anything configurable)
Place at modules/hosts/nixos/<name>/default.nix (or darwin/, or common/ for cross-platform). Use the options + config = lib.mkIf cfg.enable {…} shape:
{ config, lib, pkgs, ... }:
let cfg = config.<name>;
in {
options.<name> = {
enable = lib.mkEnableOption "<name>";
# ... typed options
};
config = lib.mkIf cfg.enable { /* ... */ };
}
Auto-imported by lib.custom.scanPaths in modules/hosts/nixos/default.nix. Host enables it with <name>.enable = true; — no import line. See modules/hosts/nixos/znapzend/default.nix for a real example with submodule types and sops wiring. Full template: references/recipes.md Recipe 2.
Why: options + scanPaths means the module ships with the framework; turning it on or off lives in the host file where humans look first. Use this over Recipe 1 whenever the feature has parameters.
Recipe 3 — add a custom package
Create pkgs/common/<name>/package.nix with a derivation:
{ lib, stdenv, fetchFromGitHub, ... }:
stdenv.mkDerivation { pname = "<name>"; version = "..."; src = ...; ... }
Auto-discovered by nixpkgs.lib.packagesFromDirectoryRecursive in flake.nix and by the additions layer in overlays/default.nix. Available immediately as pkgs.<name> everywhere and as .#packages.<system>.<name> for external builds. Real examples: pkgs/common/cd-gitroot/, mlnx-tools/, rdma-exporter/. Full template: references/recipes.md Recipe 3.
Recipe 4 — override an existing nixpkgs package
Edit the modifications block in /Volumes/Codes/nix-src/nix-config/overlays/default.nix:
modifications = final: prev: {
<pkg> = prev.<pkg>.overrideAttrs (old: { /* ... */ });
};
For Linux-only overrides use the linuxModifications block instead. To pin a package to nixpkgs-unstable, write final.unstable.<pkg> (the unstable-packages layer exposes that). Full template: references/recipes.md Recipe 4.
Recipe 5 — add a home-manager program or dotfile (primary workflow)
Three placement choices, ordered by reach:
- Cross-platform, always on for this user →
home/<user>/common/core/<name>.nix, then add./<name>.nixto the imports list inhome/<user>/common/core/default.nix. Example:home/channinghe/common/core/git.nix. - Platform-specific, always on → put in
home/<user>/common/core/nixos.nixordarwin.nix— those files are already imported bydefault.nixvia./${platform}.nix. - Opt-in per host →
home/<user>/common/optional/<category>/<name>.nix. The per-host filehome/<user>/<HostName>.nixthen adds the import. Example:home/channinghe/common/optional/desktops/niri.nixenabled only by hosts that importcommon/optional/desktops.
Why this split: core is the always-on baseline; optional gives per-host granularity without forking the user's whole config. The host file is again the audit point. Full template: references/recipes.md Recipe 5.
Adding a host
NixOS: scaffold + remote install via just spawn <Name> -d <ip> --disk-layout <layout>. Darwin: create hosts/darwin/<Name>/default.nix + home/<user>/<Name>.nix, then run just switch locally on that Mac. Full file templates and the spawn state machine: references/recipes.md Recipe 7 "Add a new host (brief)" and references/commands.md.
Adding a user
There is no dedicated recipe — mirror an existing user. Steps:
- Copy
hosts/common/users/channinghe/tohosts/common/users/<new>/. Keep the three-file split (default.nix,nixos.nix,darwin.nix); Nix does not auto-pick by platform — the host has to explicitly import the right sibling. - Replace username, hashed password reference, SSH keys, and home directory. Gate Linux-only attrs with
lib.optionalAttrs pkgs.stdenv.isLinux { group = "wheel"; }so Darwin still evaluates (see Critical conventions). - Copy
home/channinghe/tohome/<new>/. Keepcommon/core/{default,nixos,darwin}.nixand anycommon/optional/<cat>/subtrees the new user needs. - In each host that gets the new user, import
hosts/common/users/<new>/default.nixand the matching platform file (e.g.hosts/common/users/<new>/nixos.nix), then addhome/<new>/<HostName>.nixto the host's home-manager users. - Tell the user which secrets to add to
nix-secrets: hashed password key, SSH public key list. The user runssopsand.sops.yamlupdates — seereferences/nix-secrets.md.
Concept: referencing a secret in Nix code
You write only the Nix reference below; YAML and .sops.yaml edits are user-operated (see references/nix-secrets.md). In the module, declare the secret and reference its decrypted path:
sops.secrets."<path/in/yaml>" = {
sopsFile = "${inputs.nix-secrets}/secrets/<HostName>.yaml"; # or shared.yaml
owner = "root";
mode = "0400";
};
# use: config.sops.secrets."<path/in/yaml>".path
Tell the user which YAML key to add (e.g. "komodo/core_public_keys") and which file (shared.yaml for cross-host, <HostName>.yaml for single-host). For templated rendering (netrc, env files, anything where the secret is one substring of a larger format), use sops.templates with config.sops.placeholder."<path>" — see references/recipes.md Recipe 8.
Critical conventions
system.stateVersiontypes differ by platform. NixOS expects a string:system.stateVersion = "25.05";. Darwin expects an integer:system.stateVersion = 6;. Mixing them throws a type error in evaluation. The values track the initial install version and must never be bumped without reading the NixOS / nix-darwin release notes — they pin migrations, not the running version.lib.custom.relativeToRoottakes a string, not a Path literal. Source:lib.path.append ../.applied to a string fragment. Writinglib.custom.relativeToRoot ./hosts/common/corefails; uselib.custom.relativeToRoot "hosts/common/core". The repo always wraps it inmap … [ "a" "b" "c" ]for that reason.scanPathsauto-imports,optional/does not.modules/hosts/{common,nixos,darwin}/<x>lights up the moment its file lands.hosts/common/optional/<x>is inert until a host config names it. The split is intentional: modules define capabilities, optionals describe one machine's choices.lib.mkDefaultin base layers,lib.mkForcewhen overriding a value already wrapped in mkDefault elsewhere. Many hostSpec fields and home settings use mkDefault so hosts can replace them with a bare assignment; if a host needs to win against another optional that also uses mkDefault, escalate to mkForce. Real example:hostSpec.scaling = lib.mkForce "1"inhosts/nixos/Annulatus/default.nix.- Darwin users have no
groupattribute. When writing a shared user config underhosts/common/users/<u>/, gate Linux-only attrs withlib.optionalAttrs pkgs.stdenv.isLinux { group = "wheel"; }(andextraGroups,uid,gidsimilarly). Otherwise the Darwin evaluation errors atusers.users.<u>.groupunknown option. useAtticCache = falsefor bootstrap hosts. Default is true. A fresh host with no LAN cache reachability hangs the first rebuild on the binary cache; sethostSpec.useAtticCache = falseuntil it can resolve the cache. Existing hosts with cache access leave it true.- Cross-platform home wiring is an explicit import.
home/<u>/common/core/default.nixliterally writes./${platform}.nix. Renamingnixos.nixordarwin.nixbreaks evaluation silently because the path interpolates fromhostSpec.isDarwin. - The three-file pattern in
hosts/common/users/<u>/is convention only. Nix does not infer thatnixos.nixis for NixOS; the host config is expected to also importhosts/common/users/<u>/nixos.nix(ordarwin.nix) explicitly. Search any host file forusers/channinghe/nixos.nixto confirm the pattern before adding a new user. - Secrets are user-operated. This skill writes Nix code that references
sops.secrets."<path>"and tells the user what YAML key to populate. It does not runsops, edit.sops.yaml, generate age keys, or rekey. The user does those steps and reports back.
Commands cheat sheet
The most common targets. Full catalog (27 just targets) and the nix-config/scripts/spawn.sh state machine: references/commands.md.
just switch # switch current host (nh os/darwin switch via nix-config/scripts/rebuild.sh)
just check # nix flake check
just update # nix flake update
just spawn <Name> -d <ip> --disk-layout <ext4|zfs|btrfs|zfs-mirror>
# full state-driven NixOS install via nixos-anywhere
just deploy <Name> # remote update via deploy-rs (auto-rollback)
just deploy-all # deploy every NixOS host via deploy-rs (batch rollback on failure)
nix develop # enter shell with sops, age, ssh-to-age, just, gum
References
Read these on demand. Each is self-contained; do not chain reads. Section pointers quote exact headings so grep-by-title works.
references/recipes.md— The main reference. Decision tree, full file templates for adding host / service / module / package / overlay / home program / desktop / secret-using service, plus edge cases. Read this for any "add X" task before touching files. Section pointers: "Recipe 1: Add a drop-in optional service (no options)", "Recipe 2: Add a reusable module with options", "Recipe 3: Add a custom package", "Recipe 4: Override an existing nixpkgs package", "Recipe 5: Add a home-manager program or dotfile", "Recipe 6: Add a desktop (system + user halves)", "Recipe 7: Add a new host (brief)", "Recipe 8: Wire a service that needs a secret", "Common pitfalls".references/architecture.md—flake.nixoutputs,lib.customsemantics (relativeToRoot,scanPaths,bootDiskLayout), the cherry-pick vs auto-import mechanics, host evaluation order, the inheritance diagram. Read when something imports unexpectedly or a module fails to load.references/hostspec.md— completehostSpecoption enumeration with types, defaults, and which file populates which field. Read when wiring a new module that readsconfig.hostSpec.<something>or when an assertion inmodules/common/host-spec.nixfires.references/nix-secrets.md—nix-secretsschemas (networkInfo,serviceInfo,networkStorageInfo,sshClientsInfo,personal), the.sops.yamlstructure, known secret paths, and the consumption patterns. Reference-only — the user edits these files and runssops. Read when you need to tell the user what YAML key to add or which file to put it in.references/commands.md— fulljustfilecatalog,nix-config/scripts/rebuild.shargument forms,nix-config/scripts/spawn.shstate machine (UNREACHABLE,NON_NIXOS,INSTALLER,INSTALLED_THIS,INSTALLED_OTHER) and resumable--from/--onlyflags, deploy-rs targets. Read when running or composing commands.