14 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Repo purpose
Flake-based NixOS configuration for Wayne's LAN servers and workstation. There is no application code here — changes are Nix module edits that affect real machines when deployed.
Safety rules (read before touching anything)
- Never run
nixos-rebuild switch|boot|test,nixos-install,parted,mkfs,mkswap,swapon,mount, or any other destructive disk/deploy command from an agent session, even if asked indirectly. Deployment is done manually by the operator on the target host. - Validation is limited to evaluation, linting, formatting checks, and
nix build --dry-run --no-link. - Do not add secrets, tokens, private keys, or new password hashes to the repo.
- This repo currently contains committed password hashes in
modules/installer/common.nix(the auto-installer's own root/nixos login — a deliberate, documented choice, seedocs/auto-installer.md, not accidental tech debt) and SSH public keys invariables.nix(vars.adminSshKey,vars.remoteBuilderAuthorizedKeys) plus a couple of per-hostKEYvalues for beszel-agent auth (hosts/server/host.nix,hosts/nix-cache/host.nix). Don't use the installer's hardcoded hash as a template for a real host — every other host uses sops-nix (hashedPasswordFile, see "Security Notes" inREADME.md). Flag any new secret-like string you encounter instead of committing it. host-keys/is gitignored — locally-generated private SSH host keys for the auto-installer (seedocs/auto-installer.md). Never commit its contents; ifgit statusever shows it as trackable, something is wrong.
Commands
# One-time environment bootstrap (installs Nix if missing, prints hosts)
bash scripts/codex-setup.sh
# Full validation: secret grep, nixpkgs-fmt --check, statix lint, eval all hosts
bash scripts/codex-maintenance.sh
# Same, plus a dry-run build (no result symlink) of every host's toplevel
bash scripts/codex-maintenance.sh dry-run
# List the hosts the flake currently exposes
nix eval --json .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]'
# Evaluate a single host without building (fast sanity check)
nix eval .#nixosConfigurations.<host>.config.system.build.toplevel.drvPath --raw
# Dry-run build a single host
nix build --dry-run --no-link .#nixosConfigurations.<host>.config.system.build.toplevel
Formatting/lint tools (nixpkgs-fmt, statix) are not installed locally; the
maintenance script pulls them via nix run github:NixOS/nixpkgs/nixos-25.11#<tool>.
There is no test suite — "correctness" here means the flake evaluates and
nixpkgs-fmt/statix are clean.
In an interactive agent session, prefer targeted checks over full-repo
sweeps: after editing one or two hosts/modules, evaluate just the
nixosConfigurations.<host> you touched (plus any config.system.build.tarball
/diskoImagesScript/package output affected) rather than looping over every
host — codex-maintenance.sh evaluates every nixosConfigurations host plus
every package/tarball/image variant and is slow to run after each small
change. Reserve a full
codex-maintenance.sh run for changes that plausibly affect every host
(modules/common/*, flake.nix, variables.nix) or as a final check before
committing. This is a session-workflow preference only — it does not apply to
CI, which should keep running the full script on every push/PR regardless of
diff size; that's the point of it.
Scripts
Beyond codex-setup.sh/codex-maintenance.sh above, scripts/ also has:
scripts/sync-host-keys.sh— generates/registers SSH host keys and their.sops.yaml/secrets/*.yamlrecipients for flake targets, idempotently (--all,<target>,--remove,--regenerate-all-keys, all with--dry-run). The primary tool for provisioning a new host's secrets access — see "Creating a new machine" indocs/auto-installer.md.scripts/prepare-host-key.sh— narrower predecessor: generates a key by an arbitrary name without touching.sops.yaml. Still useful to pre-generate a key before its flake target exists yet, sincesync-host-keys.shcan only act on targetsnixosConfigurationsalready has.scripts/create-proxmox-resource.sh— builds alxc-*/proxmox-*target's tarball/disk image and creates it on a real Proxmox node (pct createagainst the tarball as a CT template /qm create+importdisk), or reconfigures an existing resource's cores/memory/disk size (--modify, always requires typing the VMID back to confirm). Checks for an already-uploaded image on the node before building (--force-rebuildto skip that and always rebuild), and probes nix-cache's substituter/remote-builder reachability once up front rather than letting everynix buildcall retry against it individually. Refuses to create a target whose host identity already exists live on the node (checked directly viaqm/pct, not any file in this repo) unless--allow-duplicate-hostis passed.--dry-runthroughout both modes.scripts/env.sh— shared config (PROXMOX_HOST, storage pool, bridge, default cores/memory) sourced bycreate-proxmox-resource.sh. Add new cross-script config here instead of duplicating it per-script.scripts/bump-nixpkgs-release.sh— bumpsflake.nix'snixpkgs.url/home-manager.urlin place. Exists because flake input URLs can't referencevariables.nix(confirmed empirically —nix flake metadataerrors on it), so this is the closest equivalent to a single source of truth for the tracked release.scripts/rotate-admin-key.sh <backup-admin-key> [--new-key-file <path>] [--dry-run]— rotates.sops.yaml's&adminage key: decrypts with a backed-up copy of the key currently trusted as&admin(verified by deriving its public key and comparing, not taken on faith), replaces the&adminline with a new key already present in the environment (defaults to wherever sops/age itself would look), and runssops updatekeyson everysecrets/*.yaml. One-way: the old key can no longer decrypt anything re-encrypted this way. This is the automation for the manual stepssync-host-keys.sh/create-proxmox-resource.shprint when they bootstrap a brand-new, not-yet-trusted key on a machine with no prior admin access.scripts/backup-admin-key.sh <dest-path> [--key-file <path>] [--force] [--dry-run]— copies the local sops age key (source resolution matches sops/age itself:$SOPS_AGE_KEYinline, then--key-file, then$SOPS_AGE_KEY_FILE, then the XDG default) to an arbitrary destination path with0600permissions, validating it's a real age identity and round-tripping the public key before and after the write. Refuses to overwrite an existing<dest-path>without--force. Purely a local filesystem copy — never touches.sops.yaml/secrets/*.yamlor the repo at all. The resulting file is exactly whatrotate-admin-key.shexpects as its backup-key argument.
sync-host-keys.sh, create-proxmox-resource.sh, and
rotate-admin-key.sh genuinely mutate real state when run for real (not
--dry-run): real secrets/*.yaml recipients, real Proxmox VMs/
containers, real revocation of decrypt access. They require the
operator's own SSH/sops access, which an agent session doesn't have — but
don't suggest running any of them non-dry-run without the operator's
explicit go-ahead even if it becomes technically reachable.
backup-admin-key.sh only writes a key copy to a path the operator gives
it — lower-stakes than the others, but it still handles a real private
key, so treat its destination path choice as the operator's call too.
Architecture
flake.nix is the single entry point. It generates one
nixosConfigurations.<platform>-<buildtype> attribute per target via the
mkTarget function, composed from:
nixosSystem {
modules = [
disko.nixosModules.disko
sops-nix.nixosModules.sops
./modules/common/configuration.nix
./modules/platforms/${platform}.nix # what it runs on
./modules/build-types/${buildType}.nix # what it's for
hostPath # hosts/<name>/host.nix — per-machine identity
home-manager.nixosModules.home-manager { ... }
] ++ (client-only modules, for every buildType except "nix-cache" itself)
}
Platforms: linode, proxmox, lxc. Build types: minimal, nix-cache,
server, docker, gui, pxe-boot, tailscale-exit-node. Not every
combination is built — e.g. pxe-boot has no linode variant (PXE/DHCP/TFTP
need LAN L2 adjacency a Linode VPS doesn't have). Treat flake.nix's
generatedTargets as the source
of truth for which hosts exist — README.md, AGENTS.md,
docs/flake-lock-automation.md, and the CI eval workflows
(.github/workflows/check-nixos.yml, .gitea/workflows/check-nixos.yml) list
hosts by hand (or, for the CI workflows, evaluate the flake dynamically) and
can drift from it, so re-check them against flake.nix when adding or
removing a host.
Composition pattern
hosts/<name>/host.nix— per-machine identity only: hostname, hostId, per-machine secrets,system.stateVersion. These files carry noimportsof their own beyond narrow parameterized helpers (seemodules/beszel/host-token.nixbelow) — all shared behavior comes from the platform/build-type modules composed inflake.nix, not from the host file.modules/platforms/{linode,proxmox,lxc}.nix— platform-specific config: boot method, guest tooling, and (for linode/proxmox) the hypervisor-specific hardware config, imported directly by the platform module itself (../hardware-configuration/vm/{proxmox,linode}.nix) — not wired in fromflake.nix.lxc.nixhas no hardware-configuration counterpart since containers share the host kernel; instead it imports nixpkgs' ownvirtualisation/proxmox-lxc.nix, which gives everylxc-*host aconfig.system.build.tarballoutput — a plain rootfs tarball, used as apct create ... vztmplCT template (notpct restore, which expectsvzdumpbackup-archive metadata this doesn't have), no install step — seedocs/auto-installer.md.modules/build-types/*.nix— what a system is for: minimal/server/docker/gui/pxe-boot/nix-cache/tailscale-exit-node.modules/common/configuration.nix— base NixOS config imported by every host: locale, users, nix settings, git.modules/common/home.nix/hosts/nixos/home.nix— Home Manager config for thenixosuser; thenixosworkstation (guibuild type) has its own, other hosts sharemodules/common/home.nix.modules/disko/proxmox.nix— declarative disk layout (GPT: ESP + swap + ext4 root) via disko, used by all Proxmox-VM hosts (proxmox-*, notlxc-*). Also carriesimageSize/imageName, letting everyproxmox-*host be built as a standalone,qm importdisk-ready.rawimage with no install step — seedocs/proxmox-images.md.modules/disko/linode.nix—linode-*'s disko config, deliberately different in kind from the Proxmox one: Linode provisions and sizes/dev/sda//dev/sdbitself as whole, unpartitioned devices before the OS boots, so this declares them withdestroy = false(disko never wipes them) and a barefilesystem/swapcontent type instead of a partition table — idempotent against an already-provisioned disk, never destructive.modules/boot/efi.nix— systemd-boot + EFI vars, paired with the disko module.modules/installer/— the auto-installer environment (ISO, also served as PXE netboot):common.nix(shared config + the generatedauto-install.sh),iso.nix,host-keys.nix(optionally bakeshost-keys/into the image under--impure). Seedocs/auto-installer.md.modules/pxe-boot/stage-installer-artifacts.nix— builds the installer's netboot image and stages it on thepxe-boothost so its iPXE menu can chain straight to it. Seedocs/pxe-boot.md.modules/nix-cache/{client,server,remote-builder-client}.nix— binary cache substituter + SSH remote-builder wiring; seedocs/nix-cache.mdfor the full design (per-host local stores, no shared/nix/store, and how thenixremotesigning/SSH keys fit together).modules/beszel/host-token.nix— parameterized helper module ({ name, sopsFile }) that wires a host's beszel-agent sops secret/template andenvironmentFile; used byhosts/server/host.nixandhosts/nix-cache/host.nixto avoid duplicating that boilerplate.modules/tailscale/,modules/docker/,modules/networking/,modules/traefik/,modules/services/*— single-purpose, single-host feature modules (e.g.docker/enable-service.nix,services/zfs/enable-service.nix). Grepmodules/build-types/*.nixfor each build type'simportslist to see which modules apply where.
New host = new hosts/<name>/host.nix + a matching
mkTarget { platform; buildType; hostPath; } entry added to flake.nix's
generatedTargets, composed from existing modules/* pieces rather than
duplicating config.
Other docs worth reading before touching these areas
docs/nix-cache.md— nix-cache binary cache/remote-builder design and key handling.docs/pxe-boot.md— thepxe-boothost's iPXE/TFTP/HTTP boot chain and directory layout under/srv/pxe.docs/auto-installer.md— the installer environment (ISO/netboot/Proxmox LXC),host-keys/and the sops-nix pre-seeding problem it solves, and whylxc-*hosts are deliberately excluded from its menu.docs/proxmox-images.md— buildingproxmox-*hosts as standalone.rawdisk images (disko's image builder) instead of installing, and deploying the result to Proxmox.docs/flake-lock-automation.md— howflake.lockupdates flow through CI (schedulednix flake updatePR + host-eval-on-PR workflow) and why hosts should track the committed lock file rather thannixos-rebuild --upgrade-all.