Introduce `modules.network`, the host-level networking foundation a Host
declares once. A Host states its trunk interface and the tagged VLAN ids to
materialize, and the module emits one systemd-networkd bridge per VLAN, named
by the `br-vlan<id>` convention, plus the host's own management address on a
chosen VLAN's bridge.
The trunk and every bridge set `RequiredForOnline = "no"` so wait-online never
blocks boot on a carrier-less link, and the module owns its own NetworkManager
`unmanaged` guard so enabling it is self-sufficient. Two assertions tie the
management VLAN to the declared VLANs and a management address to a VLAN, so an
address can never be silently dropped.
Enabled on neogaia as a tracer with placeholder values, proving the host build
and bridge-name evaluation through `nix flake check`. No Guest is wired to a
bridge yet.
Add a third auto-loaded kind beside the host and the module: the guest, a
reusable definition under guests/ that a host enables like a module and that
realizes its interior as a systemd-nspawn nested container.
Split the shared base config so a guest can stand on it.
base.nix now holds the substrate both bases share — the primary user,
home-manager, and the unstable/stable overlays.
system.nix keeps the host-only machinery, and a new guest.nix is the slim
guest-base: it imports the full modules tree, pins the interior release, and
auto-enables the toolkit bundle and SSH so any guest is workable on sight.
Give modules.ssh a guest flavor.
A host restores its host keys from secrets as before, while a guest sets
hostKeys.restore = false, names no sops files, and self-generates a host key,
so it holds no age key of its own.
The lib grows a guest helper that declares the guests.<path> namespace with an
enable and a backend field.
Only the container backend is built; microvm is a reserved value that trips a
clear build-time assertion rather than silently building nothing.
A sample guest exercises the whole path, and neogaia enables it, so the guest
interior builds through the existing nix flake check seam.
Add modules.toolkit, an aggregator that enables fish, tmux, nvim, git,
and direnv as one unit, and makes fish the login shell, so any Host or
Guest shell feels identical. Each member is set via mkDefault, so a Host
can still override any single piece while the one flag brings up the
whole bundle.
neogaia now enables the bundle as a single line in place of its five
individual enables and its explicit default-shell line, keeping the Host
a flat checklist.
Also lands the domain model, ADR 0006, and the guests spec: the toolkit
is the first piece of the wider guests work, and the guest-base will
auto-enable this bundle.
Add a standalone modules.desktop.obsidian module and enable it on
neogaia. Give it a waybar window-rewrite icon so its windows show a
recognisable glyph on the workspace indicator.
Apply a codebase-wide comment audit against the comment conventions:
split banned semicolons and multi-sentence lines into one sentence per
line, cut cross-file and history narration, trim file-top headers to a
single purpose line, and drop verbosity that did not earn its place.
Prose docs (CLAUDE.md, install.md) get the same one-sentence-per-line
and no-semicolon treatment.
The task 0031 commit moved the module files but a mis-staged `git add`
left the option-path rewrites out, so main declared the old namespaces
(`modules.claude-code`, `modules.gitea-axi`, `modules.pi`,
`modules.desktop.hyprlock`/`hypridle`) while CONTEXT.md and ADR 0004
already documented the new ones. Rewrite the paths in the moved and
in-place modules, the neogaia host, and the two live CLAUDE.md gotchas
so the code matches the recorded convention.
Introduce the modules/desktop/ group behind a single modules.desktop.enable
aggregator that hand-lists and turns on each piece at default priority, so a
host enables the whole desktop with one flag yet can override any single piece.
The Hyprland compositor lives in a subdirectory of the group, sourced from
nixpkgs with the program integration owning the session, portals, and polkit
and home-manager writing the config against that one package. greetd with the
tuigreet greeter provides a mouse-free text login that launches the session
through uwsm, and Ghostty is the terminal on Super+Return.
Port the operator's KDE/i3 bindings expressed entirely in hjkl and letters,
tune input (US layout, Caps->Escape, snappy repeat, touchpad taps and natural
scroll, flat mouse accel), and set a subtle feel with blur left as a host knob.
Enable the desktop on neogaia.
Add a `pi` module that turns on the home-manager `programs.pi-coding-agent`
integration for the primary user and freezes a single settings.json: the
default provider set to Anthropic, the default model to Opus (claude-opus-4-8,
Pi's own catalogue id), and analytics disabled. Everything else in
~/.pi/agent — the credential and all self-modification state — is left
unmanaged, so login survives rebuilds and backing Pi out is a one-line enable
flip. Enabled on neogaia alone.
Add gitea-axi as a flake input and a new modules.gitea-axi module that
installs the CLI through its home-manager module. That module also declares
the Claude Code context — the Agent Skill and the SessionStart dashboard hook
— when claude-code is enabled on the host, so neogaia gets both while a host
without the harness would get the CLI alone.
Drop the hand-written SessionStart hook from the claude-code module, which
named a gitea-axi binary nothing installed; the gitea-axi module owns it now.
Remove the vendored skill fork, which had drifted from upstream and would
collide with the copy the module writes.
The access policy was a separate fleet declaration mapping each machine to a
role and a client key, which the module looked up by hostname to derive what to
authorize. Authorizing a key needs the key text and nothing else, so the
per-machine names, the hostname lookup and the role existed only to reconstruct
a grouping that two lists already are.
The module now exposes those two lists and the set a machine admits, and a host
declares what it draws from in its own file. The fleet declaration is gone, and
with it a single-file directory at the repository root.
`authorizedKeys` defaults to the workstation keys. An option of a list type is
not mandatory the way a scalar one is: undeclared, it yields the empty list
rather than failing, and a machine admitting no key is unreachable. The default
makes the safe case the silent one.
Verified to produce the same system as the design it replaces, and against the
running machine in both directions.
The operator's SSH client key existed only as a file created by hand on one
laptop, so a reimage would destroy it and lock the operator out of the remote.
It now lives in neogaia's own secrets file, encrypted to the admin identity and
neogaia alone, and the client is pointed at the decrypted path rather than a
copy in the user's home.
Access becomes a policy over roles instead of a per-host list of keys. A new
fleet declaration names each machine's role and client public key, and every
machine derives what it authorizes from that: a workstation admits workstations
alone, a server admits both, so a compromised server reaches no machine of the
operator's own. Registering a machine is an entry in that one file.
Only neogaia exists, so the server half of the policy is built rather than
exercised. Two assertions reject a machine missing from the fleet and any entry
whose role no policy defines.
Defaulting the module on hid it from a host's config, leaving no single
place that lists what a host carries. Each host enables it explicitly
instead, at the cost of a step when adding one.
Default the module on rather than restating it per host, so a new host
commits without remembering the line. A host that should not carry a
personal identity sets enable to false.
Git identity lived only in one checkout's local configuration on one
machine, so it was invisible to every other checkout and lost on a
reimage. Declare it as a module instead, enabled on neogaia.
It is a module rather than base plumbing so a host that should not carry
a personal commit identity can decline it.
The sops secret declarations, the empty `hostKeys`, and the `HostKey`
lines were boilerplate sitting in the host, and every future host
restoring its identity would have repeated them.
`modules.ssh` takes the encrypted file and the key types, deriving the
secret names and the `HostKey` lines from one list, so the daemon and the
secrets cannot disagree about where a key lives. The resulting system
derivation is unchanged.
Also record two gotchas: reading PR review comments needs the Gitea API
rather than tea, and host keys are not user authentication keys.
The host's SSH identity was generated by the daemon, so reimaging the
laptop would invalidate it and break `known_hosts` for every client that
had ever connected.
Add `secrets/neogaia.yaml`, the first per-host secrets file, encrypted to
the admin identity and `neogaia` alone so a compromised machine cannot
decrypt another's material. It holds both host private keys; the public
halves are committed in plaintext.
Clear `services.openssh.hostKeys` to stop generation and point `HostKey`
at the decrypted paths. These are ordinary secrets rather than the
pre-user ones the password uses, exercising the second decryption path.
The detection file was written before the machine ran NixOS, as an educated
guess at what a Dell XPS 13 9380 needs, and said so. Its contents now come
from nixos-generate-config on the running machine.
The guess was wider than the measurement rather than narrower: it named
thunderbolt, usb_storage and sd_mod, none of which the scan reports, and
omitted rtsx_pci_sdmmc. Nothing needed to reach the root device was missing.
Filesystem, swap, LUKS and microcode declarations from the scan are dropped.
The disk layout derives the first three and the hardware profile supplies the
last, so carrying them here would duplicate definitions that are produced on
every evaluation.
Each generation stores a kernel and an initrd on the EFI system partition,
and this host runs a large third-party kernel, so 512 MiB holds only about
seven. An exhausted partition fails at bootloader installation, after the
build has already succeeded.
The partition cannot grow in place: it sits first on the disk with the
encrypted container immediately behind it, and an encrypted volume's start
offset cannot move without rewriting its whole payload. Only an install
applies this layout, so the change takes effect at the next reimage and
does nothing to the running machine until then.
Record that a flake only sees git-tracked files, which cost an evaluation
earlier, and correct the git identity gotcha that a hand-written gitconfig
had made false.
The laptop's hardware facts were guessed before it ran NixOS. Hand them to
the upstream nixos-hardware profile for this exact model, which fixes four
things that are wrong on the running machine: the laptop suspends into
s2idle rather than deep S3, the PS/2 mouse driver loads over an i2c
touchpad, no thermal management runs, and firmware updates are impossible.
The profile is taken wholesale, including the Intel graphics support it
carries. Those packages are inert without a display server, and trimming
them would mean diverging from upstream for no present benefit.
Drop the host's own Intel microcode setting, which the profile now defaults
from the redistributable firmware already enabled here.
The input follows the base nixpkgs: only its NixOS modules are consumed, so
its own pin would be evaluated by nothing while drifting silently.
The two acceptance criteria needing a reboot are left open; the sleep mode
and module blacklist only take effect on a fresh boot.
Sets the xkb layout and `caps:escape_shifted_capslock` in the shared base
config, and builds the console keymap from it so the remap applies on a
bare TTY rather than only under a graphical session. Shift+Caps Lock
still toggles Caps Lock.
neogaia's `console.keyMap` is dropped: `console.useXkbConfig` defines
that option itself, so the two definitions would conflict.
Cut restated "what", domain-glossary framing, cross-file consumption
narration, and against-alternative justification from in-file comments;
keep only non-obvious "why" and load-bearing pointers. Drop the
`generateCompletions` line (a no-op restatement of the upstream default)
and its comment.
Install Claude Code through home-manager's native programs.claude-code
module, matching how the tmux and fish Modules use their home-manager
options rather than a raw home.packages entry. The feature Module at
modules/claude-code/claude-code.nix stays thin — just the enable option
and the delegation — and writes no settings, so login and first-run
configuration remain interactive and no auth material enters the repo.
Signing in without a browser (needed over the console or SSH) is
documented in modules/claude-code/authentication.md: the paste-code OAuth
flow, where the printed URL is opened on another device and the code
pasted back, and the ANTHROPIC_API_KEY path for non-interactive use.
The neogaia toplevel builds with claude-code-2.1.209 included.
Add an nvim Module that configures Neovim declaratively through nixvim,
wired as a flake input and consumed as its home-manager module. Options,
globals, keymaps, and plugin settings are typed Nix; the colorscheme
call and two autocmds live in modules/nvim/config.lua via extraConfigLua.
Plugins come from nixpkgs (no plugin manager, no runtime cloning); git,
ripgrep, and fd are provided from Nix; treesitter grammars are built by
Nix so no runtime compiler is needed.
Functionally matches the previous config (plugins, keymaps, options, the
nord colorscheme, markdown conceal, the Neogit blame toggle), verified
headless against the generated init.
Configure tmux through home-manager's programs.tmux: the settings it exposes
as options (prefix, keyMode, mouse, baseIndex, clock24, escapeTime,
historyLimit, terminal) are set as options, and every setting it has no option
for is read verbatim from modules/tmux/extra.conf. No tmux plugin manager is
used. The generated config is behaviourally identical to the reference, verified
by parsing it with a live tmux binary.
Move the login-shell switch behind a new modules.fish.defaultShell
option (default false; neogaia opts in). Convert every non-eza alias to
an abbreviation, set preferAbbrs and pin generateCompletions, and switch
the command line to vi-style editing. Cap navigation at four dots and
drop psmem/psmem10/dir/vdir/please. Comment the installed packages.
Translate the CachyOS fish snapshot into a home-manager Module: the
fastfetch greeting, the bat-backed manpager, the done and bang-bang
plugins (from nixpkgs, not a plugin manager), the history/backup/copy
helper functions, and the eza and navigation aliases. Pacman-specific
aliases are dropped or replaced with NixOS equivalents (update ->
nixos-rebuild switch, cleanup -> nix-collect-garbage). Enabling the
Module also makes fish the user's default login shell.
Turn the booted laptop into a usable, remotely reachable console: enable
NetworkManager so wifi can be joined from the console and an SSH daemon so
the rest of the setup can be driven over the network. Set the base locale to
timezone America/New_York, locale en_GB.UTF-8, and console keymap us.
These live inline in the neogaia Host rather than behind enable Modules,
matching the single-Host MVI precedent; a shared locale/networking Module can
be extracted once a second Host needs the same settings. SSH keeps default
password auth on, as first-boot access relies on the hand-set bootstrap
password until key/sops-based auth lands post-install.
modules/zram.nix only wrapped the native zramSwap.enable toggle without
adding anything. Rewrite the touched comments to describe only the current
file content, and record the in-file-comment convention in CLAUDE.md.
Select the CachyOS kernel per-Host via boot.kernelPackages, enable Intel
microcode and redistributable firmware (ath10k for the QCA6174 wifi), and
move zram behind a toggle Module. Declare the chaotic binary cache in the
base Nix settings (extra-substituters/keys) so the built system fetches the
kernel from nyx-cache rather than compiling it.
Add disko as a flake input (following nixpkgs) and wire its NixOS module
into the host-builder, so any Host can declare a disk layout while the
layout itself stays a per-Host concern.
neogaia's layout (hosts/neogaia/disk.nix): one GPT NVMe disk with a
systemd-boot EFI system partition and a LUKS container holding btrfs with
@root/@home/@nix subvolumes. The initrd prompts for the passphrase on a
normal boot. Swap is RAM-backed zram, so there is no on-disk swap
partition; zramSwap is enabled directly for now. The placeholder
fileSystems are removed since disko now derives them.
In-file comments should describe only what the file currently is, not task
numbers, external tooling, or past/future states.
- hosts/neogaia: drop the "replaced by disko" / "arrive later" framing;
describe the filesystems and hardware profile as the placeholder values
they are.
- lib: drop the "no null-placeholder traversal hack" comparison to a prior
implementation.
- system: drop "no impure environment lookup" from the user.name description
and the sops/post-boot roadmap from the user comment.
26.05 is the latest stable NixOS release as of now (26.11 is still the
in-development branch that nixos-unstable reports); the previous 25.05 pin
was a release behind.
- flake.nix / flake.lock: stable overlay tracks nixos-26.05.
- neogaia system.stateVersion and the base home.stateVersion set to 26.05
(fresh install, so aligning to the current stable release).
- Refresh the "latest stable release" references in the project docs to
match.
Other inputs are rolling branches (nixos-unstable, nixpkgs-unstable,
chaotic nyxpkgs-unstable) or master (home-manager, per spec), so they carry
no version to bump.
Stand up the walking skeleton the rest of the laptop MVI extends and
re-verifies against: the whole neogaia Host evaluates and its system
toplevel builds (nix flake check green).
- flake.nix: hand-rolled flake (no flake-parts). Base nixos-unstable, plus
nixpkgs-unstable and nixos-25.05 for the per-package unstable/stable
overlays, home-manager (nixpkgs followed), and chaotic-nyx (deliberately
not following our nixpkgs, to keep its binary cache usable). checks build
each Host toplevel.
- lib/: trimmed helper lib — the Auto-loader (recursive .nix discovery, no
null-placeholder hack), the host-builder, and the script-from-file helper.
Deps inherited explicitly; no with lib.my, no nixosModules output.
- system/: shared base config — the unstable/stable overlays, the user
option (defaults to alexion, in wheel, drives system + home-manager user
in lockstep), flakes, git, and home-manager as a NixOS module.
- modules/example.nix: Auto-loader / Enable-convention reference Module,
inert until enabled.
- hosts/neogaia/: minimal laptop Host — placeholder filesystems, bootloader,
and hardware profile.
- CLAUDE.md: project agent instructions with a Gotchas section (nix on the
CachyOS dev host, the chaotic overlay/cache behaviour, the Gitea CLI).