docs: correct false claims in the README

Verified against nix eval, upstream sources and a local test:

- testvm accepts SSH password authentication (NixOS default), the README
  claimed the opposite
- only /boot goes through by-partlabel; the filesystems inside the container
  come up as /dev/mapper/crypted
- kanshi parses an empty config fine; a missing one is the error case, in the
  README and in the desktop module comment
- modules/user.nix ABOUTME claimed a login name shared by every host
- the flake imports six modules, not five

Rewrites "Keeping a machine up to date": the checkout is ~/flakes/moonix and
/etc/nixos/flake.nix a symlink to it, which man 8 nixos-rebuild documents.
Drops the git and ssh-keygen basics. Adds an optional LUKS header backup step,
and records the checkout decision in DECISIONS.md.
This commit is contained in:
2026-08-20 12:09:23 +02:00
parent 205e3eb60f
commit 14d16ee8ec
3 changed files with 244 additions and 49 deletions
+110 -12
View File
@@ -1,8 +1,106 @@
# Decisions
## 2026-08-20 The checkout on a machine lives in `~/flakes/moonix`
- **Who**: Maintainer, ClaudeCode
- **Why**: `disko-install` leaves no checkout behind, and the README documented
one that was invented rather than derived: a clone in the home root plus a
symlink, justified with reasons that did not hold. The configuration has to be
editable and committable on the machine it describes, with the account's
identity on the commits and the account's key on the push.
- **Tradeoffs**: `/etc/nixos` is `nixos-rebuild`'s default path, but a repository
there belongs to root: commits would be root's and pushing would need root's
key. Handing the directory to the account with `install -d -o` fixes that by
making a system directory user-writable, which is a workaround rather than a
layout. `/opt/moonix` would mirror `moonarch`, which archinstall clones to
`/opt/moonarch`, but that copy is root-owned and never committed from —
these files are. `~/Repos` is the scratch area for development checkouts on
the workstation, not a permanent location. `~/.config/flakes` is
configuration space for programs reading it on this machine; the repository is
not read as configuration, it is the source a system is built from.
- **How**: The checkout is `~/flakes/moonix`, owned by the account, and
`/etc/nixos/flake.nix` is a symlink to its `flake.nix`. `man 8 nixos-rebuild`
documents exactly this: the file "may be a symlink to a flake.nix in an actual
flake; thus /etc/nixos need not be a flake", and `--flake` "defaults to the
directory containing the target of the symlink". `sudo nixos-rebuild switch`
therefore needs no arguments and picks the host by hostname. The `flakes`
directory is plural on purpose: `moonarch` leaving its `flake = false` state,
dev flakes for the compiled projects and devShells would live beside it. A
machine that is never edited locally keeps no checkout and builds from the
Gitea reference instead, which is what `disko-install` did.
## 2026-08-20 Desktop host, /home on a second disk
- **Who**: Maintainer, ClaudeCode
- **Why**: The desktop has a 512G system disk and a 1T disk, and `/home`
belongs on the large one. `modules/disk.nix` only knew a single disk, so the
layout had nowhere to put the second.
- **Tradeoffs**: A single btrfs spanning both disks would be one filesystem and
one LUKS container, but it ties the two together: losing either loses the
pool, and the system disk stops being restorable on its own. Mounting the 1T
somewhere under `/mnt` and leaving `/home` small keeps the module untouched,
at the price of configs and data living apart and snapper needing two configs
with different meanings. A second disk carrying `/home` as its own btrfs
keeps both disks independent and leaves the existing subvolume names alone.
- **How**: `moonarch.disk.home.enable` adds a second GPT with one partition,
wrapped in LUKS `crypted-home` when `encrypt` is set, holding the `/home`
subvolume. The main disk drops `/home` in that case, so the subvolume exists
exactly once. No second ESP: the firmware boots from the main disk. Both
disks want the same passphrase — systemd's initrd caches the first one and
retries it on the second container, so the boot prompts once even though
formatting asks four times. `hosts/desktop` declares deliberately invalid
device paths, because `disko-install --disk main|home` overrides them and a
forgotten flag should fail rather than erase the wrong disk. Without a
`nixos-hardware` profile the host sets `enableRedistributableFirmware` and
`hardware.cpu.amd.updateMicrocode` itself, and forces `services.fwupd` off —
desktop mainboard vendors mostly do not publish to the LVFS.
## 2026-08-20 ThinkPad: redistributable firmware and the fingerprint reader
- **Who**: Maintainer, ClaudeCode
- **Why**: `hardware.enableRedistributableFirmware` was never set. The
`nixos-hardware` profile does not set it either — `common/cpu/amd`
reads it (`hardware.cpu.amd.updateMicrocode = lib.mkDefault
config.hardware.enableRedistributableFirmware`), so with the option off the
machine ran on the microcode in its BIOS and without the firmware the WLAN
card and amdgpu load.
- **Tradeoffs**: The firmware is redistributable but not free, so enabling it
is a deliberate step rather than a default. Declining it means no microcode
updates on a CPU whose errata are fixed exactly there.
- **How**: `hardware.enableRedistributableFirmware = true` in the host file,
which the profile's `mkDefault` then turns into a microcode update.
`services.fprintd.enable` alongside it — the NixOS module wires `pam_fprintd`
into the PAM stacks it owns; a finger is enrolled with `fprintd-enroll` after
installing.
## 2026-08-19 Initial password in the host file, account name as an option
- **Who**: Maintainer, ClaudeCode
- **Why**: An install that leaves the account without a password produces a
system whose lock screen never opens — swaylock authenticates through
pam_unix. The documented remedy, `nixos-enter --root /mnt -c passwd`, is the
path that fails after `disko-install` with `chmod: changing permission of
'/mnt/dev': Operation not permitted` (disko issue #638, closed as not
planned). Alongside that, the host files carried a real name in five places.
- **Tradeoffs**: `initialPassword` puts a plaintext password in the repository.
Weighed against `hashedPasswordFile`, which keeps the secret out of the repo
but needs the file on the target disk before the first boot — either through
`disko-install --extra-files` or a `cp` into the still-mounted `/mnt`. Both
are extra steps in exchange for protecting a value that is meant to be
replaced on first login anyway. The 2026-08-11 decision to keep any password
out of the repository is therefore reversed for the initial one.
- **How**: `initialPassword = "moonarch"` in both hosts, `mutableUsers` left at
its default so `passwd` overrides it permanently. New `modules/user.nix`
declares `moonarch.user` without a default, because the name belongs to the
machine; `users.users`, the autologin `initial_session.user` and snapper's
`ALLOW_USERS` all read it, so the login name exists once per host instead of
five times. README loses the `nixos-enter` step and the stale "Known gaps"
section — `Super+C` reaches the Quickshell clipboard popout, which is wired up
and whose `cliphist` backend is installed.
## 2026-08-14 moonset and moonlock dropped; the power menu is a Quickshell popout
- **Who**: Dominik, ClaudeCode
- **Who**: Maintainer, ClaudeCode
- **Why**: The power menu became `PowerPopout.qml` in the moonarch Quickshell
config (see `moonarch/DECISIONS.md`, same date), and moonset was the last thing
invoking moonlock. Both packages therefore leave this flake with nothing to
@@ -21,7 +119,7 @@
## 2026-08-14 regreet and swaylock from nixpkgs, autologin after LUKS
- **Who**: Dominik, ClaudeCode
- **Who**: Maintainer, ClaudeCode
- **Why**: moongreet and moonlock were dropped as self-maintained projects; the
reasoning is in `moonarch/DECISIONS.md` for the same date. Here the change is
mostly subtraction: two flake inputs, two build recipes and a generated config
@@ -69,7 +167,7 @@
## 2026-08-11 Disk layout via disko after all, as a shared module
- **Who**: Dominik, ClaudeCode
- **Who**: Maintainer, ClaudeCode
- **Why**: The Calamares route decided below turned out to be six manual steps,
one of which — copying `hardware-configuration.nix` into the repo — fails
silently unless the file is also `git add`ed, because Nix only sees tracked
@@ -91,12 +189,12 @@
generated file was deleted, having described the UUIDs of a qcow2 that no
longer exists.
- **Consequence**: The user password no longer arrives with the installer.
`users.users.dkressler` declares none, so it is set once via `nixos-enter`
`users.users.<name>` declares none, so it is set once via `nixos-enter`
after installing rather than committing a hash to the repository.
## 2026-08-11 Swapfile without hibernation
- **Who**: ClaudeCode, on Dominik's request for a recommendation
- **Who**: ClaudeCode, on the maintainer's request for a recommendation
- **Why**: The layout module needs a swap default.
- **Tradeoffs**: Hibernation needs swap at least the size of RAM plus a
`resume_offset` kernel parameter. On btrfs that offset changes whenever the
@@ -110,7 +208,7 @@
## 2026-08-07 Separate repo instead of Nix files in the application repos
- **Who**: Dominik, ClaudeCode
- **Who**: Maintainer, ClaudeCode
- **Why**: The Nix build recipes had to live somewhere without disturbing the
existing repos.
- **Tradeoffs**: Files inside each project repo would keep version, code and
@@ -122,7 +220,7 @@
## 2026-08-07 Configs deployed as files, not translated to Nix options
- **Who**: Dominik
- **Who**: Maintainer
- **Why**: One set of config files should stay valid on NixOS and Arch alike.
- **Tradeoffs**: Native Nix options (`programs.niri.settings`) are idiomatic and
type checked, but every config would exist twice and drift on each change.
@@ -131,7 +229,7 @@
## 2026-08-07 No Home Manager
- **Who**: Dominik
- **Who**: Maintainer
- **Why**: Initially proposed for the three home-directory tasks in
`post-install.sh` (GTK4 symlinks, `gsettings`, seeding the stasis config).
- **Tradeoffs**: None in favour, as it turned out. Every one of those tasks is
@@ -146,7 +244,7 @@
## 2026-08-07 FHS paths fixed in the source, not bridged with symlinks
- **Who**: Dominik
- **Who**: Maintainer
- **Why**: moonlock, moongreet and moonset invoked `systemctl`, `loginctl` and
`moonlock` through absolute `/usr/bin` paths, and moongreet scanned
`/usr/share/{wayland-,x}sessions`. All of it fails on NixOS.
@@ -163,7 +261,7 @@
## 2026-08-07 waybar and swaync not installed
- **Who**: Dominik
- **Who**: Maintainer
- **Why**: Quickshell provides both the bar and the notifications. The config
source still carries waybar and swaync, marked "kept as a reserve" — a note
left over from an earlier migration.
@@ -176,7 +274,7 @@
## 2026-08-07 QEMU/KVM as the test host, with a caveat
- **Who**: Dominik, ClaudeCode
- **Who**: Maintainer, ClaudeCode
- **Why**: VirtualBox was the first choice, but its 3D acceleration is
documented as broken with Wayland compositors.
- **Tradeoffs**: QEMU needs no DKMS module on `linux-zen` and virtio-gpu
@@ -190,7 +288,7 @@
## 2026-08-07 Disk layout via Calamares, not disko
- **Who**: Dominik
- **Who**: Maintainer
- **Why**: Stage 3 targets bare metal, but no target machine exists yet —
neither the number of disks nor their device paths are known.
- **Tradeoffs**: `disko` would put partitioning and encryption under version
+129 -33
View File
@@ -23,6 +23,7 @@ pkgs/ Build recipes
sweet-cursors.nix cursor theme
moonarch-scripts.nix battery and camera helpers
modules/
user.nix moonarch.user option, set per host
disk.nix partitioning: GPT, optional LUKS2, btrfs subvolumes
desktop.nix /etc deployment, package set, theme
greetd.nix greetd + regreet, autologin per host
@@ -30,10 +31,11 @@ modules/
hosts/
testvm/ QEMU test VM
thinkpad/ ThinkPad T14 Gen 3 (AMD)
desktop/ self-built AMD desktop, second disk for /home
```
`nixosModules.moonarch` is the entry point: it imports all three modules and
hands them the packages and the config source.
`nixosModules.moonarch` is the entry point: it imports disko's NixOS module
alongside the five above and hands them the packages and the config source.
## Usage
@@ -43,13 +45,15 @@ Build a single package:
nix build .#stasis
```
Build a whole system without activating it:
Build a whole system without activating it, which is also how a host is checked
for evaluation errors without touching anything:
```bash
nix build .#nixosConfigurations.testvm.config.system.build.toplevel
```
Activate:
Activate. This runs on the machine being changed, out of a checkout that sits
there — see "Keeping a machine up to date" for how that checkout gets there:
```bash
sudo nixos-rebuild switch --flake .#testvm
@@ -70,6 +74,7 @@ the NixOS ISO and run one command.
```bash
sudo nix --experimental-features "nix-command flakes" run \
'github:nix-community/disko/latest#disko-install' -- \
--write-efi-boot-entries \
--flake 'git+https://gitea.moonarch.de/nevaforget/moonix.git#thinkpad' \
--disk main /dev/disk/by-id/nvme-SKHynix_HFS001TEJ9X102N_SDB7N7740101A8708
```
@@ -77,20 +82,20 @@ the NixOS ISO and run one command.
The LUKS passphrase is asked for twice while formatting. The repo is fetched
from the flake reference, so the ISO needs no `git` and nothing is cloned.
2. Set the user password. Nothing has created one yet. Autologin gets past the
greeter without it, but swaylock authenticates through pam_unix and cannot
unlock a screen for an account that has no password:
`--write-efi-boot-entries` is not optional here. Without it `disko-install`
forces `boot.loader.efi.canTouchEfiVariables` off, overriding the `true` in
the host file, and the install finishes without an NVRAM boot entry. Leave it
out only when the disk is installed in one machine and then moved to another.
```bash
sudo nixos-enter --root /mnt -c 'passwd dkressler'
```
2. Reboot.
3. Reboot.
To skip step 2, put a `mkpasswd -m sha-512` hash into
`users.users.dkressler.initialHashedPassword` — the commented-out line in
`hosts/thinkpad/default.nix` shows where. That commits a password hash to the
repository.
The account comes up with the initial password declared in the host file
(`users.users.<name>.initialPassword`, currently `moonarch`). Autologin gets
past the greeter either way, but swaylock authenticates through pam_unix and
cannot unlock a screen for an account without a password, so one has to exist
from the first boot. Change it with `passwd` afterwards — `mutableUsers` is at
its default, so that sticks and the declared value is only ever used to create
the account.
`hardware-configuration.nix` is not used and not needed. NixOS puts `nvme`,
`ahci`, `sd_mod` and the USB HID modules into the initrd by default
@@ -98,9 +103,11 @@ repository.
flake, and the `nixos-hardware` profile for the T14 Gen 3 (AMD) covers
firmware, microcode, power management and graphics quirks.
The generated `fileSystems` entries point at `/dev/disk/by-partlabel/`, so the
booted system does not care whether the disk enumerates the same way next time.
Only the partitioning step reads `moonarch.disk.device`.
Only the partitioning step reads `moonarch.disk.device`. Nothing the booted
system uses depends on it: the ESP entry and the generated
`boot.initrd.luks.devices` point at `/dev/disk/by-partlabel/`, and the
filesystems inside the container come up as `/dev/mapper/crypted`. Either way
the disk may enumerate differently next time.
### Adding another machine
@@ -117,9 +124,32 @@ Only the partitioning step reads `moonarch.disk.device`.
`encrypt = false` drops the LUKS layer, `swapSize = ""` the swapfile, and
`espSize` resizes the ESP. Everything else is shared.
A machine with a second disk for `/home` adds `home`, which moves the
subvolume off the first disk:
```nix
moonarch.disk.home = {
enable = true;
device = "/dev/disk/by-id/...";
};
```
Set the login name in the same file. `moonarch.user` has no default and is
read by `users.users`, the autologin session and snapper, so the name is
declared once per machine.
3. Add a `nixosConfigurations.<name>` entry in `flake.nix`.
4. Install with the same `disko-install` command, with `#<name>` and that
machine's disk.
machine's disk. A second disk is another flag:
```bash
--disk main /dev/disk/by-id/... --disk home /dev/disk/by-id/...
```
Both disks are erased. `disko-install` overrides whatever paths the host
file declares, which is why `hosts/desktop` carries invalid placeholders:
forgetting a flag fails the install instead of erasing a disk that was not
meant to be touched.
Without a matching `nixos-hardware` profile, set
`hardware.cpu.intel.updateMicrocode` or `hardware.cpu.amd.updateMicrocode` in
@@ -131,13 +161,79 @@ the host file — the profile is what provides it for the ThinkPad.
|---|---|---|
| ESP, `espSize` (1G) | `/boot` | vfat, `umask=0077` |
| `root` | `/` | btrfs, `compress=zstd`, `noatime` |
| `home` | `/home` | same |
| `home` | `/home` | same; on the second disk where `home.enable` is set |
| `nix` | `/nix` | same; separate to keep the store out of snapshots |
| `swap` | `/.swapvol` | swapfile, `swapSize` (8G) |
With `encrypt = true` everything below the ESP sits in a LUKS2 container named
`crypted`. Hibernation is not set up — see `DECISIONS.md`.
A second disk gets its own container, `crypted-home`, and no ESP. Both are
formatted separately, so `disko-install` asks for a passphrase four times
instead of two. Give them the same one: both are unlocked from the initrd,
where systemd caches the first passphrase and retries it on the second
container, so booting prompts once.
Worth doing once the disks are formatted, though nothing here depends on it: back
up the LUKS headers. They hold the encrypted keyslots, and a damaged header loses
the data whatever the passphrase is. A header only changes when its keyslots do,
so once after the install and again after a passphrase change is enough:
```bash
sudo cryptsetup luksHeaderBackup /dev/disk/by-partlabel/disk-main-root \
--header-backup-file /run/media/<medium>/<host>-main.header
```
A second disk carries its own header on `disk-home-home`. Whoever holds such a
file can attack the passphrase offline, so it belongs on a medium that gets put
away — not in this repository and not in a synced directory.
## Keeping a machine up to date
`disko-install` built the system from the flake reference and left no checkout
behind, so a freshly installed machine runs its configuration without holding
it. The checkout goes to `~/flakes/moonix` and belongs to the account, so commits
and pushes carry the account's identity. It is not read as configuration by
anything on the machine — it is the source a system is built from, which is why
it sits outside `~/.config`:
```bash
git clone git@gitea.moonarch.de:nevaforget/moonix.git ~/flakes/moonix
sudo ln -s ~/flakes/moonix/flake.nix /etc/nixos/flake.nix
```
`nixos-rebuild` treats `/etc/nixos/flake.nix` as if `--flake` had been given and
resolves the flake to the directory the symlink points into, so `/etc/nixos`
itself is neither a repository nor a flake. Without an attribute the
configuration is picked by hostname, which is why the `nixosConfigurations` are
named after the machines.
Changing the machine is then an edit, a build and a commit:
```bash
cd ~/flakes/moonix
$EDITOR hosts/thinkpad/default.nix
sudo nixos-rebuild switch
git commit -am 'thinkpad: ...'
git push
```
The build reads the working tree, so a change is testable before it is
committed. Nix only sees tracked files though — a new file under `hosts/` or
`modules/` has to be `git add`ed or evaluation fails on the missing path.
A machine that is never edited locally can skip the checkout and build from the
reference, which is what `disko-install` did:
```bash
sudo nixos-rebuild switch \
--flake 'git+https://gitea.moonarch.de/nevaforget/moonix.git#thinkpad'
```
Every `switch` leaves a generation behind. A bad one is left through the boot
menu, where the previous generation is still listed, or with `sudo
nixos-rebuild switch --rollback` from a system that still comes up.
## Version bumps
`stasis` is pinned to a release tag. After a new upstream release:
@@ -163,9 +259,11 @@ NixOS module owns the destination:
| `waypaper/config.ini` | wallpaper path, rewritten and seeded into `$HOME` |
| fontconfig defaults | `/etc/fonts/conf.d` is owned by the NixOS module |
`kanshi/config` is not deployed: the file is empty, kanshi rejects it and
restarts in a loop. Monitor profiles are machine specific and belong in
`~/.config/kanshi/config`; the service starts only when that file exists.
`kanshi/config` is not deployed: the file in the config source is empty, and
monitor profiles are machine specific — they belong in
`~/.config/kanshi/config`. A missing config file is what kanshi treats as an
error (`failed to parse config file`), which with `Restart=on-failure` would
spin, so the service starts only once that file exists.
Programs that read only from `$HOME` and have no system-wide fallback get a
copy seeded through `systemd.user.tmpfiles` with the `C` directive, which never
@@ -192,18 +290,16 @@ cp /usr/share/edk2/x64/OVMF_VARS.4m.fd ~/VMs/moonix-vars.fd
Then run the `disko-install` command from above inside the live session, with
`#testvm` and `--disk main /dev/vda`.
It deliberately contains no testing shortcuts — no passwordless sudo, no SSH
password authentication. Set those at runtime when needed, for example through
a drop-in under `/run/systemd/system/`, so they do not end up in version
control.
`sudo` asks for a password here like on every other host. SSH accepts one:
`services.openssh.settings.PasswordAuthentication` is at its NixOS default of
`true`, and the account's password is the `initialPassword` from the host file,
which is in this repository. The guest is reachable on the launcher's forward on
`127.0.0.1:2222` with it. Anything a test needs beyond that belongs at runtime —
a drop-in under `/run/systemd/system/`, a key in `~/.ssh/authorized_keys` — so
it does not end up in version control.
The guest needs a 3D capable virtio GPU: niri's TTY backend fails every buffer
import with `Error::DeviceMissing` otherwise, and software rendering is not a
workaround. On the current host that path has twice triggered an amdgpu hard
recovery, killing the QEMU process — shut the VM down when not testing.
## Known gaps
`Super+C` opens the clipboard history through a launcher that is not installed
here, so the binding does nothing. `cliphist` records the clipboard, only the
picker front-end is missing.
+5 -4
View File
@@ -52,10 +52,11 @@ in
"xdg/quickshell/moonarch".source = "${xdg}/quickshell/moonarch";
"xdg/foot/foot.ini".source = "${xdg}/foot/foot.ini";
"xdg/fastfetch/config.jsonc".source = "${xdg}/fastfetch/config.jsonc";
# defaults/xdg/kanshi/config is an empty placeholder — on Arch too. kanshi
# rejects it ("failed to parse config file") and restarts in a loop, so it
# is not deployed. Monitor profiles are machine specific and belong in
# ~/.config/kanshi/config.
# defaults/xdg/kanshi/config is an empty placeholder — on Arch too. Not
# deployed: monitor profiles are machine specific and belong in
# ~/.config/kanshi/config. An empty file parses fine; a missing one is what
# kanshi reports as "failed to parse config file", which is why the unit in
# modules/services.nix waits for the home config to exist.
"xdg/Kvantum/kvantum.kvconfig".source = "${xdg}/Kvantum/kvantum.kvconfig";
"xdg/qt6ct/qt6ct.conf".source = "${xdg}/qt6ct/qt6ct.conf";
"xdg/gtk-3.0/settings.ini".source = "${xdg}/gtk-3.0/settings.ini";