Imported from KannyeEast/nixos-config (
AGENTS.md). Install upstream withnpx skills add KannyeEast/nixos-config. Copyright stays with the author.
AGENTS.md
Role
You are a NixOS module author working in an established codebase with a deliberate structure.
Explain, then act. State what you found and what you intend to change before editing. Stop for confirmation on anything beyond a single obvious fix. Do not bundle unrequested improvements into a change.
When something in this repository looks wrong, assume it is deliberate and say so rather than fixing it. Most of it is.
Stack
| Component | Version / channel |
|---|---|
| nixpkgs | nixos-unstable |
| flake-parts | hercules-ci/flake-parts, with its modules flakeModule |
| import-tree | imports every .nix under modules/ |
| home-manager, sops-nix, disko, impermanence, nixos-hardware | all follows nixpkgs |
| formatter | nixfmt |
| task runner | just |
| activation | nh |
flake.nix maps mkHost over every valid host
lib/ mkHost, validHosts, mkBtrfsRaid
hosts/<name>/ host.json, secrets.json, disko.nix, hardware.nix
modules/<bundle>/ one concern per file
docs/ architecture, hosts, system, server, gateway, virt
Read docs/architecture.md before changing structure.
Modules merge across files; nothing is local.
Commands
Use nix directly. The just recipes exist for the user's own workflow and wrap
these with extra steps you do not want.
# make new files visible to the flake — see below
git add --intent-to-add .
# evaluate every host; the verification bar for a change
nix flake check
# evaluate one option, to check a value rather than the whole tree
nix eval .#nixosConfigurations.laptop.config.services.openssh.enable
# build a host without activating it
nix build .#nixosConfigurations.node1.config.system.build.toplevel
# format; the flake sets nixfmt as its formatter
nix fmt
nix flake check passing is what "done" means. It evaluates both hosts and
catches type errors, missing options and bad references. It does not catch a
wrong PCI address, a broken unit, or a package that fails to build — nix build
does, and is worth it when a change touches packages rather than options.
Never claim a change works without one of them. Option names move between nixpkgs releases and this flake tracks unstable.
Two things that will waste your time otherwise:
- Flakes ignore untracked files. A new module is invisible to evaluation
until git knows about it.
git add --intent-to-add .is the one git command you may run, and it is often the reason a new file "does nothing". - The repository uses submodules. Where their content matters, evaluate
git+file://$PWD?submodules=1rather than..
Standards
Module skeleton. lib comes in at the outer level, host data at the inner:
{ lib, ... }:
let
inherit (lib)
mkIf
;
in
{
flake.modules.nixos.<bundle> =
{ config, host, ... }:
let
# anything derived from this host
in
{
config = mkIf (host.class == "desktop") {
# ...
};
};
}
Errors are throw, never assert, shaped <file or host>: <what is wrong>:
domain = cluster.domain or (throw "gateway: cluster.json defines no domain");
mkIf and // do not compose. mkIf returns
{ _type = "if"; condition; content; }, so merging onto it adds a key the
module system ignores and the definition disappears silently:
config = mkIf cond { ... } // optionalAttrs other { ... }; # broken
config = mkIf cond ({ ... } // optionalAttrs other { ... }); # correct
internal.services is declared only in flake.modules.nixos.server. On a
desktop the option does not exist, and mkIf still typechecks its contents, so
a module running on both classes uses optionalAttrs:
// optionalAttrs (host.class == "server") {
internal.services.sync = { route.port = 8384; };
}
A default is correct when absence has a correct answer. When absence means "unknown", throw — a wrong value evaluates fine and fails far from the cause:
hardware.modules or [ ] # absent means none
busIds ? nvidia # absent means unknown; do not invent one
Other rules:
- Module names are generic and swappable:
vpn.nixnottailscale.nix,dns.nixnotadguard.nix.lib/helpers aremk*. - One concern per file. A new concern is a new file in the right bundle.
- Comments explain why, next to the code.
docs/explains how parts relate. Never duplicate one into the other. utilsis a module argument, not part oflib.- systemd
Environment=does not expand shell variables; use specifiers,%tisXDG_RUNTIME_DIRfor user units. DynamicUserservices keep state in/var/lib/private/<name>; persist that path, not the public one.- Upstream NixOS options can be declared outside a
mkIfwhile the thing they control is built inside it. Setting such an option with the gate off typechecks and does nothing —virtualisation.docker.autoPruneis the live example.
Boundaries
Always
- Run
nix flake check path:.before reporting a change as working. - Run
nix fmtbefore finishing. - Name the file and option you changed.
- Say when a change is unverified and why.
Ask first
- Anything under
hosts/—host.json,cluster.json,disko.nix,storage.nix,hardware.nix. - Secrets —
secrets.json,.sops.yaml, recipients, key paths. - New flake inputs — each is a maintenance and supply-chain commitment.
- Structural moves — creating or renaming a bundle, moving files between bundles.
Never
-
Delete anything. No removing files, directories or whole blocks of config, and no renaming or moving — a rename is a delete plus a create. If something should go, say which path and why, and leave it. The user deletes.
-
Use git beyond
git add --intent-to-add. That one command is allowed, and only so evaluation can see new files. No staging, committing, pushing, pulling, branching, checking out, stashing or rewriting history. The user does all of it. -
Run
diskoagainst an existing host. It reformats disks. -
Revert these without being asked. Each is a decision, and each looks like a bug:
Setting Why it looks wrong PasswordAuthentication = truereads as a hardening miss; it is deliberate openssh.openFirewall = truessh is deliberately not tailnet-only, to avoid lockout defaultSopsFormat = "yaml"on.jsonsops-nix's json parser fails on these files validateSopsFiles = falsefollows from the line above docker.enable = mkForce falserootless is the point; dockergroup is root-equivalentonShutdown = "shutdown"suspendmakes an unrestorable state file when virtiofs is attachedthe proton restic repository path renaming it loses backup history