variables.nix's deployedTargets was a manually-maintained list with no enforcement keeping it in sync with reality -- it caused two separate false refusals in a row (naming a VM as deployed well after it had been destroyed, then matching a target against itself once the list was "corrected"). Static files can't track whether a resource still actually exists. create-proxmox-resource.sh's duplicate-host guard now queries the Proxmox node directly (qm/pct's own name/hostname config, matched against --host) instead. Also fixes a gap in that live check: it originally swallowed ssh failures and would have silently treated "can't reach the node" the same as "checked, nothing there" -- it now refuses instead of guessing when the node can't be reached. deployedTargets is removed entirely from variables.nix since nothing else in the repo consumed it once this script no longer does; README.md's Hosts table remains the sole source of truth for "(real, deployed)" status. CLAUDE.md and the script's own --help/comments updated to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
12 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 18 hosts plus every package/tarball/
image variant now 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.
sync-host-keys.sh and create-proxmox-resource.sh genuinely mutate real
state when run for real (not --dry-run): real secrets/*.yaml
recipients, real Proxmox VMs/containers. They require the operator's own
SSH/sops access, which an agent session doesn't have — but don't suggest
running either non-dry-run without the operator's explicit go-ahead even
if it becomes technically reachable.
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.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.