NixOS, nix-darwin, and Home Manager flake for managing multiple systems declaratively. Uses mixin pattern for composable configuration modules. Builds workstations, servers, VMs, macOS systems, and custom ISO images.
Build and switch system configuration:
just host # Build and switch NixOS/nix-darwin config
just home # Build and switch Home Manager config
just switch # Switch both system and home (runs home first, then host)Apply pre-built configurations from FlakeHub Cache (no local evaluation):
just apply # Apply both system and home from FlakeHub Cache
just apply-home # Apply Home Manager only
just apply-host # Apply NixOS/nix-darwin onlyBuild only (no switch):
just build-host # Build NixOS/nix-darwin only
just build-home # Build Home Manager only
just build # Build both (runs home first, then host)Validate configurations:
just check # Run nix flake check with trace
just eval # Evaluate flake syntax and all configurations
just eval-flake # Evaluate flake structure only
just eval-configs # Evaluate all system configurationsOther commands:
just build-pkg firefox # Build package for current host
just build-pkg firefox vader # Build package for specific host
just iso # Build nihilus ISO image
just update # Update flake.lock
just gc # Clean old generations, keep latest 5Install and deploy:
just install vader 192.168.1.10 # Remote install via nixos-anywhere
just inject-tokens 192.168.1.10 # Inject tokens to ISO host for installjust install handles SOPS age key injection, SSH host key decryption, and LUKS password/keyfile setup. Optional parameters: keep_disks="true", vm_test="true". just inject-tokens sends user age key and host age key to the target. Optional parameter: user="nixos".
Language:
- British English spelling throughout all documentation and code
- Comments use full sentences with proper punctuation
File structure:
- Shared cross-platform configuration in
common/default.nix, imported by both NixOS and nix-darwin - Configuration organised in
_mixinsdirectories using mixin pattern - Each mixin is self-contained with
default.nixentry point - Host-specific configs in
nixos/{hostname}/,darwin/{hostname}/ - Custom packages in
pkgs/directory, exposed via overlay
Shell scripts:
- All scripts use
pkgs.writeShellApplicationwrapper (provides shellcheck validation) - Scripts live in
nixos/_mixins/scripts/orhome-manager/_mixins/scripts/ - Each script in separate directory:
script-name/default.nix+script-name.sh - Runtime dependencies declared in
runtimeInputslist - Template available at
home-manager/_mixins/scripts/_template/
Naming conventions:
- Hostnames: Sith Lords (workstations/servers), TIE fighters (VMs)
- Variables: camelCase for Nix attributes
- Files: kebab-case for directories and Nix files
- Functions: camelCase in
lib/flake-builders.nix
Nix style:
- Use
nixfmtformatter (run vianix fmt) - Prefer
lib.mkDefaultandlib.mkForceover plain values for overridability - Use
lib.optionalandlib.optionalsfor conditional imports - Explicit
inheritstatements for clarity - String interpolation:
"${variable}"notvariable
Module shorthand convention:
- Use
inherit (config.noughty) host;inletbindings (NOTcfg) - Then reference
host.is.workstation,host.desktop,host.name, etc.
Mixin placement:
- System-level services, kernel, boot, networking →
nixos/_mixins/ - User-level programs, dotfiles, scripts →
home-manager/_mixins/ - Hardware-specific config (disks, kernel modules) →
nixos/{hostname}/ - Use
home.packagesin Home Manager modules,environment.systemPackagesin NixOS modules
All systems defined in lib/registry-systems.nix (imported by flake.nix). Each entry specifies:
- kind (required):
"computer","server","vm","container" - platform (required):
"x86_64-linux","aarch64-darwin", etc. - formFactor (optional):
"laptop","desktop","handheld","tablet","phone" - desktop (optional): derived from
kind+ platform if omitted (e.g.computeron Linux defaults to"hyprland", Darwin defaults to"aqua") - username (optional): defaults to
"martin" - gpu (optional):
{ vendors = [ "amd" "nvidia" ]; compute = { vendor = "nvidia"; vram = 16; }; } - displays (optional): list of display submodules with output, width, height, refresh, scale, position, primary, workspaces
- tags (optional):
[ "streamstation" "thinkpad" "iso" ... ]
Users defined in lib/registry-users.nix (imported by flake.nix):
# In lib/registry-users.nix
{
martin = { tags = [ "developer" ]; };
}ISO hosts use tags = [ "iso" ] which applies implicit defaults (desktop = null, username = "nixos"). The ISO host is nihilus.
Helper functions in lib/flake-builders.nix generate configs from the registry. resolveEntry merges four layers: baseline username, kind+OS derived desktop, ISO implicit defaults, then explicit entry values.
All host/user metadata is accessed via config.noughty.* options, not specialArgs. The noughty module provides type checking, defaults, and mkDefault/mkForce overridability.
Only four values remain in specialArgs:
inputs,outputs: flake inputs and outputsstateVersion: NixOS/Home Manager state versioncatppuccinPalette: colour palette helper
Access host/user data in modules:
{ config, noughtyLib, lib, ... }:
let
inherit (config.noughty) host;
in
lib.mkIf host.is.workstation {
# host.name, host.desktop, host.is.laptop, host.gpu.hasNvidia, etc.
}Use noughtyLib helpers for tag and identity checks:
{ noughtyLib, lib, pkgs, ... }:
lib.mkIf (noughtyLib.isUser [ "martin" ]) {
home.packages = [ pkgs.zed-editor ];
}Key gating patterns:
host.is.workstation,host.is.server,host.is.laptop,host.is.iso,host.is.vm,host.is.darwin,host.is.linux- derived booleansnoughtyLib.isUser [ "martin" ]- user identity checknoughtyLib.isHost [ "vader" "phasma" ]- host identity checknoughtyLib.hostHasTag "streamstation"- host tag checknoughtyLib.userHasTag "developer"- user tag checkhost.gpu.hasNvidia,host.gpu.hasCuda- GPU checkshost.display.primaryOutput,host.display.isMultiMonitor- display checks
See lib/noughty/README.md for the complete option reference and usage patterns.
Secrets encrypted with sops-nix using age keys.
- User key:
~/.config/sops/age/keys.txt - Host key:
/var/lib/private/sops/age/keys.txt - Edit secrets:
sops secrets/secrets.yamlorsops secrets/host-{hostname}.yaml - Rekey after adding recipients:
sops updatekeys secrets/secrets.yaml - Never commit unencrypted secrets. All sensitive data in encrypted
.yamlfiles insecrets/.
For ISO installs, just inject-tokens sends age keys to the ISO media at /tmp/injected-tokens/. Both user and host age keys are hard requirements for install-system (hard stop if missing). FlakeHub Cache is auto-detected: install-system checks determinate-nixd status and prompts the user to run determinate-nixd login interactively if not already authenticated; otherwise falls back to local build.
Catppuccin Mocha palette available via catppuccinPalette helper:
{ catppuccinPalette, ... }:
{
# Get hex colour with #
backgroundColor = catppuccinPalette.getColor "base";
# Get hex without # (for Hyprland)
hyprlandColor = catppuccinPalette.getHyprlandColor "blue";
# Access raw palette
palette = catppuccinPalette.colors;
# Theme detection
isDark = catppuccinPalette.isDark; # true for mocha
preferShade = catppuccinPalette.preferShade; # "prefer-dark"
}Available colours: base, mantle, crust, surface0, surface1, surface2, overlay0, overlay1, overlay2, subtext0, subtext1, text, lavender, blue, sapphire, sky, teal, green, yellow, peach, maroon, red, mauve, pink, flamingo, rosewater.
Create pkgs/my-package/default.nix:
{ lib, stdenv, fetchurl, ... }:
stdenv.mkDerivation rec {
pname = "my-package";
version = "1.0.0";
src = fetchurl {
url = "https://example.com/${pname}-${version}.tar.gz";
sha256 = lib.fakeSha256; # Build once to get real hash
};
meta = with lib; {
description = "Package description";
homepage = "https://example.com";
license = licenses.mit;
platforms = platforms.linux;
};
}Add to pkgs/default.nix:
{
my-package = pkgs.callPackage ./my-package { };
}Reference in configuration:
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.my-package ];
}Use template from home-manager/_mixins/scripts/_template/:
cp -r home-manager/_mixins/scripts/_template home-manager/_mixins/scripts/my-script
mv home-manager/_mixins/scripts/my-script/template.sh home-manager/_mixins/scripts/my-script/my-script.shEdit my-script.sh with your script logic. Edit default.nix to declare runtime dependencies:
{ pkgs, ... }:
let
name = builtins.baseNameOf (builtins.toString ./.);
shellApplication = pkgs.writeShellApplication {
inherit name;
runtimeInputs = with pkgs; [ curl jq ]; # Add dependencies here
text = builtins.readFile ./${name}.sh;
};
in
{
home.packages = [ shellApplication ];
}Script automatically validated with shellcheck during build.
Add to system registry in lib/registry-systems.nix:
# In lib/registry-systems.nix
mynewhost = {
kind = "computer"; # or "server", "vm", "container"
platform = "x86_64-linux";
formFactor = "desktop"; # or "laptop", "handheld", null
gpu.vendors = [ "amd" ]; # if applicable
tags = [ "thinkpad" ]; # if applicable
displays = [
{ output = "DP-1"; width = 2560; height = 1440; refresh = 144; primary = true; workspaces = [ 1 2 3 4 5 ]; }
];
# desktop defaults to "hyprland" for computer+linux
# username defaults to "martin"
};Create host directory and nixos/mynewhost/default.nix:
{ inputs, ... }:
{
imports = [
inputs.nixos-hardware.nixosModules.common-cpu-amd
inputs.nixos-hardware.nixosModules.common-pc-ssd
./disks.nix
];
boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" ];
}Create disk layout with Disko in nixos/mynewhost/disks.nix. Build with:
nix build .#nixosConfigurations.mynewhost.config.system.build.toplevelNo other files need updating. The registry entry flows through resolveEntry -> mkSystemConfig -> mkNixos/mkHome -> noughty.* options automatically.
Three workflows in .github/workflows/:
-
builder.yml- Triggers on pull requests and pushes to main:- Inventory -
flake-inventory.shdiscovers all buildable outputs, emitting per-platform JSON matrices - Build jobs - Parallel per-host/per-package builds using
flake-build.sh(devShells, packages, NixOS, Darwin, orphan Home Manager configs) - Publish - Pushes to FlakeHub with
include-output-paths: truefor FlakeHub Cache - Release ISO - Builds and publishes the nihilus ISO on main branch
- Inventory -
-
updater.yml- Scheduledflake.lockupdates via PR -
checker.yml- Scheduled flake lock health checks
Auto-merge of flake.lock update PRs is gated on all build jobs passing via branch protection required status checks.
Mixin pattern:
Configurations composed from small, focused modules in _mixins directories. Each mixin handles one concern (e.g., desktop environment, hardware feature, script). Mixins gate themselves using config.noughty.* conditions.
Configuration flow:
- System registry in
lib/registry-systems.nixandlib/registry-users.nixdefines all hosts and users resolveEntryinlib/flake-builders.nixmerges registry defaults (baseline, kind+OS derived, ISO defaults, explicit values)mkSystemConfigproduces the attribute set consumed bymkNixos,mkHome,mkDarwinnoughty.*options are set in the modules list;lib/noughty/default.nixcomputes derived booleans_module.args.noughtyLibprovides convenience helpers to all modulescommon/default.nixprovides shared configuration (documentation, nixpkgs, nix registry, common packages, environment variables, fish shell) imported by bothnixos/default.nixanddarwin/default.nix- Platform-specific entry points add their own imports, packages and settings
- Modules gate themselves using
config.noughty.*ornoughtyLib.*
Module gating patterns:
- Flat pattern (~95% of modules):
lib.mkIf condition { ... }as the entire module body - Long-form pattern (hub modules with
imports):{ imports = [...]; config = lib.mkIf condition { ... }; }- imports stay unconditional, each sub-module gates itself - Never use
lib.optional config.noughty.* ./fooinimports- causes infinite recursion
See lib/noughty/README.md for detailed explanation of the long-form pattern.
Overlay system:
localPackages: Custom packages frompkgs/directorymodifiedPackages: Overrides and patches to nixpkgs packagesunstablePackages: Access to nixpkgs-unstable viapkgs.unstable
Applied in order, allowing layered modifications.
Build fails with "infinite recursion":
- Check for
config.noughty.*used insideimports(must be inconfigblock, notimports) - Check for circular imports in mixin modules
Home Manager activation fails:
- Ensure Home Manager configuration doesn't conflict with NixOS
- Check file ownership and permissions
- Use
home-manager switch -b backup --flake .to backup conflicting files
Secrets not accessible:
- Verify age keys exist at specified locations
- Check recipients in
.sops.yamlmatch public keys - Run
sops updatekeysafter editing recipients
Package not found:
- Verify package added to
pkgs/default.nix - Check overlay applied in configuration
- Search nixpkgs:
nix search nixpkgs <package-name>
Shell script fails shellcheck:
- Use
pkgs.writeShellApplicationwrapper (notwriteShellScriptBin) - Add necessary runtime dependencies to
runtimeInputs - Fix shellcheck warnings (shellcheck validation automatic)
- Never modify
flake.lockdirectly; usejust update - Never change
stateVersionon existing systems (breaks compatibility) - Never commit unencrypted secrets outside
secrets/directory - Never use
environment.systemPackagesin Home Manager modules; usehome.packages - Never use
writeShellScriptBin; usewriteShellApplication(enforces shellcheck) - Never use
config.noughty.*insideimports- causes infinite recursion; use the long-formconfig = lib.mkIfpattern instead - Run
just evalbefore committing Nix changes to catch evaluation errors - Run
just buildorjust build-host/just build-hometo verify builds before switching - Keep each mixin self-contained with a single concern