From c5882f63abbe818f08376839ce4f9dffee13b7d8 Mon Sep 17 00:00:00 2001 From: Gene Liverman Date: Sun, 14 Jun 2026 20:36:16 -0400 Subject: [PATCH] Add AGENTS.md and update .gitignore for Claude Code AGENTS.md is a self-contained agent guide covering who the owner is, repo layout, host inventory, build and deploy commands for all host types (NixOS, nix-darwin, home-manager-only), secrets management via sops-nix, the private-flake pattern, the dots.ports fleet-wide port registry, code quality gates, commit hygiene, and known NixOS 26.05 breaking changes. .gitignore drops stale link/nix/config entries that no longer exist and adds rules to ignore .claude/ except for .claude/skills/, which is committed so project-specific Claude Code skills are shared with the repo. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 30 +---- AGENTS.md | 318 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 322 insertions(+), 26 deletions(-) create mode 100644 AGENTS.md diff --git a/.gitignore b/.gitignore index 7728824d..a61b0969 100644 --- a/.gitignore +++ b/.gitignore @@ -11,29 +11,7 @@ # From running nixos-rebuild build-vm *.qcow2 -# Config files that are not suitable to add to version control: -link/nix/config/.mono/ -link/nix/config/asciinema/ -link/nix/config/configstore/nodemon.json -link/nix/config/configstore/snyk.json -link/nix/config/configstore/update-notifier-nodemon.json -link/nix/config/configstore/update-notifier-npm.json -link/nix/config/configstore/update-notifier-yo.json -link/nix/config/filezilla -link/nix/config/fluidkeys/ -link/nix/config/fontforge/ -link/nix/config/gcloud/ -link/nix/config/grv/ -link/nix/config/gtk-2.0/ -link/nix/config/htop/ -link/nix/config/hub -link/nix/config/iterm2/ -link/nix/config/Microsoft\\VisualStudio Services/ -link/nix/config/NuGet/nuget.config -link/nix/config/powershell/powershellget/ -link/nix/config/puppet/ -link/nix/config/QtProject/ -link/nix/config/rclone/ -link/nix/config/tmuxinator/ -link/nix/config/wireshark/ -link/nix/config/linode-cli +.claude/* +!.claude/skills/ +!.claude/skills/** + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..5862ebaf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,318 @@ +# AGENTS.md — dots + +This is the authoritative agent guide for this repository. It is self-contained +— do not assume any parent or global AGENTS.md is available. For a human-readable +overview of the repo's purpose and layout, see `README.md`. This file covers what +agents need to know that the README does not. + +--- + +## Who You're Working With + +The owner is an experienced infrastructure engineer (SRE) who manages Linux +fleets, runs a NixOS homelab, and is comfortable in a terminal. He is **not an +application developer**. When working on application code: + +- Comment generously — future maintenance may be done by an agent without full + context, or by the owner returning to code he didn't write +- Prefer explicit over implicit — avoid patterns that require deep framework + knowledge to maintain +- Prefer simple over clever — the best solution is the one that's easiest to + understand six months later +- If something is non-obvious, explain it in a comment at the point of use + +--- + +## What This Repo Is + +A multi-system Nix flake that fully manages NixOS hosts, macOS hosts (via +nix-darwin), and home-manager-only Linux machines. Sensitive configuration lives +in a companion **private-flake** at `~/repos/private-flake` — never commit +secrets or private config here. + +--- + +## Infrastructure Preferences + +When making architectural or configuration decisions: + +- **Self-hosted over cloud** — prefer infrastructure the owner controls over + third-party SaaS or cloud services +- **Open-source over proprietary** — all else being equal, prefer open-source +- **Self-sovereign data** — avoid patterns where the owner's data is in third-party + custody unless there is no self-hosted alternative +- **Simple over clever** — the least complex solution that meets the requirements + +These are preferences, not absolute rules. External services are fine for +read-only lookups or when self-hosting is genuinely impractical. + +--- + +## Repo Layout (agent-relevant summary) + +``` +flake.nix — inputs, outputs, host wiring +lib/ — mkNixosHost / mkDarwinHost / mkHomeConfig helpers +modules/ + hosts/ - host-specific modules + nixos// — per-host NixOS config + hardware + secrets.yaml + darwin// — per-host nix-darwin config + home-manager-only/ — config for hosts running Home Manager only + shared/ - reusable modules + nixos/ — NixOS modules imported by multiple hosts + home/general/ — Home Manager defaults for all systems and config for all GUI systems + home/linux/ — Home Manager config for Linux-specific apps (NixOS and Home Manager only such as Ubuntu) + files/ — raw config files managed via xdg.configFile etc. +.claude/skills/ — project-specific Claude Code skills (committed) +``` + +--- + +## Host Inventory + +| Hostname | Type | Arch | Notes | +|---|---|---|---| +| bigboy | NixOS | x86_64 | ThinkPad P52, daily driver | +| nixnuc | NixOS | x86_64 | Home server, runs most services | +| hetznix01 | NixOS | x86_64 | Hetzner VPS, runs email + matrix, primary VPS | +| hetznix02 | NixOS | aarch64 | Hetzner VPS, build host for Raspberry Pi's | +| kiosk-entryway | NixOS | x86_64 | Lenovo Q190, headless kiosk, WiFi only (`wlp3s0`) | +| kiosk-gene-desk | NixOS | aarch64 | Raspberry Pi 4, headless kiosk, WiFi only (`wlan0`) | +| AirPuppet / Blue-Rock | macOS | x86_64 | nix-darwin managed | +| mightymac | macOS | aarch64 | nix-darwin managed | +| rainbow-planet | home-manager only | x86_64 | Ubuntu, old NixOS config currently commented out in flake | + +--- + +## Build and Deploy Commands + +The `nixup` and `nixdiff` aliases abstract away the differences between NixOS, +nix-darwin, and home-manager-only hosts — use them everywhere rather than +invoking the underlying tools directly. Their definitions live in: +- NixOS / home-manager-only Linux: `modules/shared/home/linux/default.nix` +- macOS (nix-darwin): `modules/hosts/darwin/home.nix` + +### Check what will change before deploying + +```bash +nixdiff # builds the new config and diffs it against the currently running system +``` + +### Apply locally + +```bash +nixup # NixOS: sudo nixos-rebuild switch --flake ~/repos/dots + # nix-darwin: darwin-rebuild switch --flake ~/repos/dots + # home-manager only: home-manager switch --flake ~/repos/dots#gene-x86_64-linux +``` + +There is also `nixboot` on NixOS hosts to stage a change for the next reboot +without switching immediately. + +### Build here, deploy to a remote NixOS host + +`nixos-rebuild` is available natively on NixOS hosts but not on home-manager-only +or nix-darwin hosts. When deploying from one of those, bring it in via `nix shell`: + +```bash +nix shell nixpkgs#nixos-rebuild -c nixos-rebuild switch \ + --flake .# \ + --target-host ssh:// \ + --sudo +``` + +To only build the closure without activating (useful for checking before committing): + +```bash +nix build .#nixosConfigurations..config.system.build.toplevel +``` + +### Build SD card image (kiosk-gene-desk) + +```bash +nix build .#packages.aarch64-linux.kiosk-gene-desk-sdImage +``` + +--- + +## Secrets Management + +All secrets use **sops-nix**. Encrypted `secrets.yaml` files live per-host under +`modules/hosts/nixos//` and shared secrets at `modules/shared/secrets.yaml`. + +- Age key derivation: `ssh-to-age -private-key -i ~/.ssh/id_ed25519 > ~/.config/sops/age/keys.txt` +- Key registration: `.sops.yaml` at repo root controls which age keys decrypt which files +- Edit a secret: `sops modules/hosts/nixos//secrets.yaml` + +When a secret must be readable by a non-root service user, set `owner` on the +`sops.secrets` entry (e.g. `owner = "wpa_supplicant"` for wifi credentials). +The default is `root:root 0400`. + +--- + +## Private Flake + +Sensitive host modules (wifi networks, email accounts, Tailscale keys, etc.) live +in `~/repos/private-flake`. It is a flake input and its modules are passed via +`additionalModules` in `flake.nix`. When diagnosing a host issue, always check +both repos — the private flake often holds the relevant config. + +--- + +## NixOS Module Pattern + +When a service has its own repo, the NixOS module lives there — not here: + +- The service repo's `flake.nix` exports `nixosModules.default` +- This repo adds the service repo as a flake input and imports the module in the + relevant host's `additionalModules` +- This keeps deployment config co-located with the code it deploys + +When writing NixOS modules: +- Expose typed options with `lib.mkOption` — domain, ports, data directories, + secrets file path, enable flag +- Use `DynamicUser = true` for systemd services where possible +- Pass secrets via `EnvironmentFile` pointing to a sops-managed path — never + hardcode secrets +- Add `systemd.tmpfiles.rules` entries to create required directories + +### Port Registry + +Service ports are managed through a two-level registry rather than scattered +magic numbers: + +- `modules/shared/nixos/ports.nix` — defines the `dots.ports` option type and + declares fleet-wide ports (ssh, http, https, shared service ports). All entries + default to `openFirewall = false`. +- `modules/hosts/nixos//ports.nix` — host-specific ports plus any + fleet-wide overrides (e.g. setting `openFirewall = true` for a port only that + host exposes). + +The `openFirewall` flag is not just documentation — hosts that use the registry +wire it directly to `networking.firewall` via `lib.pipe`: + +```nix +networking.firewall = { + allowedTCPPorts = lib.pipe config.dots.ports [ + builtins.attrValues + (builtins.filter (e: e.openFirewall && e.protocol == "tcp")) + (map (e: e.port)) + ]; + allowedUDPPorts = lib.pipe config.dots.ports [ + builtins.attrValues + (builtins.filter (e: e.openFirewall && e.protocol == "udp")) + (map (e: e.port)) + ]; +}; +``` + +Reference ports in config as `config.dots.ports..port` rather than +hardcoding numbers — this applies everywhere: service configs, nginx proxy +targets, container definitions, and the firewall. When the surrounding attrset +attribute name is already `port`, use `inherit` — statix rules W03/W04 +(`manual_inherit` / `manual_inherit_from`) enforce this and will fail the build +if you use the verbose form where `inherit` would work: + +```nix +# rejected by statix W04 +port = config.dots.ports.grafana.port; + +# correct +inherit (config.dots.ports.grafana) port; +``` + +When adding a service that other hosts need to know about (e.g. a shared API +endpoint referenced across hosts), declare it in the shared registry. When +adding a service local to one host, declare it in that host's `ports.nix`. + +--- + +## Code Quality Gates + +Before pushing any `.nix` change: + +```bash +nix fmt . # nixfmt-tree — formats all Nix files +nix run .#deadnix # finds dead code +nix run .#statix # lints for common issues +``` + +Pre-commit hooks enforce this automatically after `pre-commit install`. CI +(`.github/workflows/validate.yml`) mirrors the same checks. + +**Also run `nix build .#nixosConfigurations..config.system.build.toplevel`** +for the affected host before pushing whenever `flake.nix`, `flake.lock`, or a +host module changes. The Nix sandbox is authoritative; remote CI failures are +slow to diagnose. + +--- + +## Commit and PR Hygiene + +- Write commit messages as descriptions of what the code **is**, not what changed. + For non-trivial commits, include a body: one short paragraph per major component + describing what it does, key decisions, and any non-obvious constraints. A + subject line alone is not sufficient for features. Reserve before/after framing + for bug fixes only. +- Incremental local commits are fine as a working tool, but squash before pushing. + What reaches the remote should reflect the final, complete state of the work. + Multiple commits in a pushed PR are only justified for genuinely independent + logical concerns. +- If a follow-up fix is caught after committing (even after pushing), amend + immediately and silently — use `--force-with-lease` if already pushed. +- After every push that changes what a PR does, update the PR description: + `gh pr edit --body "..."`. Base it entirely on `git log main..HEAD`. +- When writing `gh pr create` or `gh pr edit` bodies containing backticks, use + `PREOF` (not `EOF`) as the heredoc delimiter — backticks inside + `$(cat <<'EOF' ... EOF)` are interpreted as command substitution by the outer + shell; `PREOF` prevents this. +- Do not bundle unrelated changes in a single commit. + +--- + +## Git Branch Workflow + +- Start new work from a fresh main: + `git checkout main && git pull`, then `git checkout -b ` +- Delete merged branches locally after the PR merges: `git branch -d ` +- Only rebase when there is actual divergence from main. Check + `git log --oneline origin/main..HEAD` before opening a PR — if main has not + moved ahead of your branch base, rebasing rewrites SHAs for no reason. + +--- + +## Skills + +Project-specific Claude Code skills live in `.claude/skills/` and are committed +to this repo (`.gitignore` tracks `skills/` but ignores the rest of `.claude/`). +Add new skills there when a workflow is worth automating. + +--- + +## What NOT to Change Without Asking + +- `flake.lock` — only update intentionally with `nix flake update [input]` +- `.sops.yaml` — changing key rules requires re-encrypting secrets; confirm before touching +- `system.stateVersion` — must stay at the NixOS version the host was first installed on +- This `AGENTS.md` file itself — propose changes rather than silently editing + +--- + +## NixOS 26.05 — Known Breaking Changes + +All hosts are on 26.05 except kiosk-gene-desk (Pi — deployment method TBD). +These notes apply when completing that upgrade or when rebuilding from scratch. + +**wpa_supplicant hardening** (affects all WiFi hosts): +- Set `networking.wireless.interfaces = [""]` explicitly — auto-detection + reads `/sys/class/net` which is not mounted in the new hardened sandbox +- Set `owner = "wpa_supplicant"` on the sops wifi_creds secret — the daemon now + runs unprivileged and cannot read root-owned files +- Service name changes from `wpa_supplicant.service` to + `wpa_supplicant-.service` — update `restartUnits` and any `wants` +- Interface names: kiosk-entryway uses `wlp3s0`, kiosk-gene-desk uses `wlan0` + +**simple-nixos-mailserver API changes**: +- `mailserver.certificateScheme` removed → use `mailserver.x509.useACMEHost` +- `mailserver.loginAccounts` renamed → `mailserver.accounts` +- `config.services.dovecot2.user` no longer exists → hardcode `"dovecot2"`