Imported from esp0xdeadbeef/nixos (
AGENTS.md). Install upstream withnpx skills add esp0xdeadbeef/nixos. Copyright stays with the author.
NixConfig Conventions for LLMs
Approach
Prefer the best + safest solution over the simplest + fastest. This config is destined for production, so thoroughness beats a quick hack: if doing it right takes longer, take that time. In particular:
- Fix the root cause; do not layer a workaround over a broken mechanism.
- Do not leave a legacy/dual mechanism behind "for back-compat" when the design is being replaced — migrate it fully, including its GAMP contracts and tests.
- Leave the repo in a clean state: no stale branches, no reformatting noise mixed into a semantic change, no half-finished migrations.
- A complete fix that lands later is better than a shortcut that ships today and needs revisiting in prod.
Network layer discipline (network-* repos and this repo's network work)
The GAMP specs are the contract. "It renders / it works" is not evidence you placed a behavior in the right layer.
- Before touching any network code or network config, READ
~/github/network-codex-agent/GAMP/URS/README.mdand~/github/network-codex-agent/GAMP/FS/README.md— the files themselves, not a summary — and state in your plan which spec sentence assigns the behavior to the layer you are about to edit (compiler / forwarding-model / control-plane-model / realization-model / renderer). - Verify the layer's real input contract from the code entry points (what it consumes and emits), not from the docs alone. Docs that contradict the code are a finding, not a license to follow the code.
- If the URS/FS does not state the responsibility, or states it incorrectly: ADD OR FIX THE REQUIREMENT IN THE URS/FS FIRST, in the same change, with the descriptive-category sentence — then implement against it. Do not invent layer behavior the spec does not assign, and do not "just make it work" in the wrong layer because the spec is silent.
- Network meaning (routes, routing style, loopbacks, NAT/NAT66, DNS policy, firewall allows, prefixes) computed in a layer the spec assigns to another layer is a bug. Cite the authorizing FS/URS sentence in the commit message.
- The intent/realization split is non-negotiable: routing style and topology choices are intent (FS-481); ASNs, peer addresses and other realization facts are inventory. Never smuggle one into the other.
- Do not parse intent shapes (nodes/uplinks/egress) in downstream layers; consume the upstream layer's normalized output.
Protected paths: prod and legacy-prod (absolute rule)
The production and legacy production network surfaces are protected. This
covers, at minimum, prod-network/prod/, prod-network/current/, any
*-prod / *-legacy-prod flake inputs and pins, and the
s-router-prod / s-router-legacy-prod / s-router-prod-* /
s-router-legacy-prod-* host configs and their secrets.
- Do not read, edit, delete, move,
git checkout,git restore,git stash, or otherwise mutate these paths without explicit, current, per-session permission from the user that names the exact files. A general task, an earlier approval, or another file's approval is not permission for these. - There are two separate permissions, and both are required:
- permission to edit the protected files, and
- permission to push the resulting commits.
- Without the edit permission, treat these paths as read-only-untouchable and leave them exactly as found. Do not "restore", "fix", or "clean up" them as a side effect of another task.
- When in doubt, stop and ask. An unauthorized touch of these paths is never a lesser error than an incomplete task.
Git safety (applies to every repo, not just network-*)
Never discard work to "get back to a baseline". The working tree is the progress. Before reverting, stashing, or resetting anything:
- Run
git status --shortandgit difffirst and read them. - Preserve the state:
git stash push(orgit diff > /tmp/work.patch). - Only then apply the minimal revert/checkout for the specific file or hunk
under investigation, never
git reset --hard/git clean -fd/git checkout -- .on the whole tree. rsync --deletemust also be preceded by capturing the destination state or scoping the sync to the exact files changed.
If the diagnosis is uncertain, capture the diff and show it before touching anything; do not guess-and-nuke.
Branch upstream
- The
ghrepo always points tomain(its default branch); keep it that way. ✗ no-upstreamin the zsh prompt just flags that the current branch has no upstream tracking branch. Fix it by pointing the branch atmain:git branch --set-upstream-to=origin/main.
Commit Messages
Conventional commits: type(scope): description
type:feat,fix,refactor,chore,WIPscope: path-based, reflecting what part of the config changed. Examples:home-manager/{feature}for home-manager features{host}or{host}/{service}for host-specific:l-envil,l-esp/ollama,s-tau/disko- Just the component for shared/global:
overlays,impermanence,nebula
- Message is lowercase, no period at end.
Directory Structure
.
├── home-manager/ # Home Manager user config per host
│ ├── 01-general/ # Shared feature modules (desktop, editors, etc.)
│ ├── 02-window-manager-i3/
│ ├── 03-window-manager-sway/
│ ├── {hostname}/ # Per-host config
│ │ ├── home.nix # Entry point
│ │ └── ...
├── nixos/ # NixOS host configs
│ ├── laptop/ # {l-hostname} hosts
│ │ └── {hostname}/
│ │ ├── default.nix
│ │ └── hardware/
│ ├── server/ # {s-hostname} hosts
│ │ └── {hostname}/
│ └── virtual-machine/ # VM hosts (nixos-shell-vm/, dedicated-vm/, etc.)
│ └── {vm-name}/
├── library/ # Importable NixOS modules
│ └── 01-general/ # Feature modules (packages/, network/, desktop/, impermanence/, etc.)
├── modules/ # Custom NixOS & HM modules (unused if using library/)
├── overlays/ # Package overlays and patches
│ ├── default.nix # Aggregates all overlays
│ ├── additions.nix # Custom packages from ./pkgs
│ ├── modifications.nix # Patched packages (xlayoutdisplay, libvirt, etc.)
│ ├── unstable-packages.nix # pkgs.unstable with ollama/python workarounds
│ └── nixpkgs-25_11-packages.nix # pkgs.nixpkgs-25_11 for ruby 3.3 compat
├── pkgs/ # Custom package derivations
├── patches/ # Patch files referenced by overlays
├── profiles/ # Composable config profiles
│ ├── default.nix # Registry of all profiles
│ ├── nixos/ # NixOS profiles
│ └── home-manager/ # HM profiles
├── secrets/ # SOPS-encrypted secrets
│ ├── hosts/ # Host-specific secrets
│ └── common/ # Shared secrets
├── prod-network/ # Network prod-pin scripts
├── .github/workflows/ # CI workflows
├── flake.nix # Flake entry point
└── .sops.yaml # SOPS encryption keys
Code Style
- Formatter: nixpkgs-fmt (
nix fmtto run). ALWAYS format after edits. Never format unmodified files. - Indentation: 2 spaces, no tabs.
- Line endings: LF, final newline, trimmed trailing whitespace.
- Nix conventions:
- Top-level modules are functions taking
{pkgs, lib, config, inputs, ...}. - Use
libfromnixpkgs.lib // home-manager.lib(merged, already inoutputs.lib). - Feature-flag modules use a
default.nixwith a booleanenableoption gating imports. - Prefer
lib.mkOption/lib.mkEnableOptionfor new options. - Overlays with external dependencies (inputs, relativeRepo) use a context wrapper:
{ inputs }: final: prev: { ... }. - Plain overlays (no deps) are bare
final: prev: { ... }.
- Top-level modules are functions taking
Secrets
- Managed with sops-nix, keys defined in
.sops.yaml. - Two types of secret files:
secrets/common/-- shared across hosts, encrypted to all host age keys.secrets/hosts/{hostname}/-- per-host, encrypted to that host only.
- Both are also encrypted to the PGP key
7088C7421873E0DB97FF17C2245CAB70B4C225E9. It lives on misterio's yubikey. - Never read secret values into context. Ask the user to read them, or use pipes and redirection so they do not appear in model-visible output, process arguments, or command history.
- When writing SOPS secrets through a pipeline, verify success without revealing
the values. For example, check exit statuses and compare byte counts or hashes
(
wc,sha256sum) while keeping secret-bearing output out of context.
Pre-push Privacy Checks
- Never commit or publish an unredacted public IPv4 address or an IPv6 address from the current public subnet.
- RFC1918 IPv4 addresses and IPv6 ULA addresses are private infrastructure and are allowed; do not treat them as public-address findings.
- Before every push, discover the current public addresses without printing them
(
curl -4 -fsS https://ifconfig.meandcurl -6 -fsS https://ifconfig.me). Check the exact IPv4 address and derive the IPv6 network using the known delegated prefix, or conservatively/64when the delegated prefix is unknown. Normalize candidate IPv6 addresses and test subnet membership; a textual prefix grep alone is insufficient. - Build a private denylist through pipes from the relevant decrypted SOPS files. Include personal names, account and email local parts, domains, email addresses, hostnames, public IP addresses, and other identity-bearing values. Do not hardcode that denylist in this file or expose its values in model-visible output, process arguments, command history, or insecure temporary files.
- Before every push, scan case-insensitively across the complete candidate tree, every outgoing commit, and staged changes for the public addresses and private denylist. Report only redacted file and line locations; never print the matched value.
- The versioned
.githooks/pre-pushperforms this scan. Keepcore.hooksPath = .githooks, do not bypass the hook, and treat an incomplete scan as a failed push. - Verify that SOPS files being pushed remain encrypted and that no derived secret value occurs in plaintext outside encrypted SOPS data. If address discovery, SOPS decryption, or any scan is incomplete, fails, or finds a match, stop and do not push.
Checking
- Prefer
rg(ripgrep) overgrepfor code search —grepis slow on this tree.grepis still allowed whenrgisn't suitable. - Format touched Nix files with
nix fmt(runs nixpkgs-fmt on all .nix files, or on explicit file args). - Run
nix flake check --all-systemsafter meaningful Nix changes.
Do not run nixos-rebuild switch or other apply/deploy commands
unless the user explicitly asks for that.
Tool documentation
When you need to know a tool's configuration syntax, option names, or data
formats (kea, radvd, hostapd, …), read the docs/examples the package ships —
don't guess. Pull the package into a shell and read its share/doc tree:
nix shell nixpkgs#kea -c bash -c 'which kea-dhcp4'
# then inspect <store-path>/share/doc/kea/ (e.g. examples/kea4/all-options.json)
Examples live under <store-path>/share/doc/<pkg>/ and are authoritative for
exact option names and formats (kea's all-options.json documents every DHCP
option, including classless-static-route 121's dst - router dash format).
Nix eval
When verifying config output before deploying:
- NixOS config:
nixosConfigurations.<host>.config.<path> - Home-manager (managed by NixOS):
nixosConfigurations.<host>.config.home-manager.users.<user>.<path> - Specialised HM variant:
nixosConfigurations.<host>.config.home-manager.users.<user>.specialisation.<variant>.config.<path> nix build <path>.source --no-link --print-out-pathsto get the built filenix eval <path> --jsonto inspect raw attribute values
Updating fetchurl packages
When a fetchurl-based derivation fails because the upstream URL 404s (Dell, etc.):
- Find the new version:\n - For a directory listing:
curl -fsSL "<base-url>/" | grep -oP '<pattern>' | sort -V | tail -5 - Get the new hash:
nix-prefetch-url "<new-url>"- The second line of output is the base32 Nix hash.
- Convert to SRI:
nix hash to-sri --type sha256 <base32-hash> - Or just let the build fail — the error message gives the correct SRI hash directly.
- Update
version,url, andhashin the derivation. - Verify:
nix build .#<package> --no-link