- Nix 64.1%
- Shell 23.4%
- Lua 7.8%
- Just 2.6%
- CSS 2.1%
Bumps LazyVim plugin pins (catppuccin, treesitter, lspconfig, mini.*, and others) to latest. |
||
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| configs | ||
| hosts | ||
| installer | ||
| modules | ||
| overlays | ||
| pkgs | ||
| scripts | ||
| .gitignore | ||
| CLAUDE.md | ||
| flake.lock | ||
| flake.nix | ||
| justfile | ||
| jz@chaos.li.pub.asc | ||
| README.md | ||
| tools.md | ||
nixconfig — home-manager flake
First-time setup
1. Install Nix (Determinate Systems installer)
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
This installs Nix with flakes enabled by default. Restart your shell after.
2. Install home-manager
nix run home-manager/master -- init --switch
Or, once your flake is ready, pick your configuration:
| Machine | Command |
|---|---|
| tux — NixOS bare-metal (UEFI), x86_64 | sudo nixos-rebuild switch --flake ~/Projects/nix/nixconfigs#tux (or hms) |
| big-rig — CachyOS/Arch, x86_64, NVIDIA | home-manager switch --flake ~/Projects/nix/nixconfigs#big-rig |
| NixOS VM, x86_64 | sudo nixos-rebuild switch --flake ~/Projects/nix/nixconfigs#nixos-vm |
| Other NixOS bare-metal (UEFI), x86_64 | Install via the installer ISO → finish-setup.sh; then sudo nixos-rebuild switch --flake ~/Projects/nix/nixconfigs#<hostname> (or hms) |
| MacBook Intel (x86_64) | home-manager switch --flake ~/Projects/nix/nixconfigs#macbook-intel |
| MacBook M2 (aarch64) | home-manager switch --flake ~/Projects/nix/nixconfigs#macbook-silicon |
3. Activate for the first time
# NixOS host (tux):
sudo nixos-rebuild switch --flake ~/Projects/nix/nixconfigs#tux
# Arch/macOS host (home-manager only):
home-manager switch --flake ~/Projects/nix/nixconfigs#big-rig
4. Subsequent updates
sudo nixos-rebuild switch --flake ~/Projects/nix/nixconfigs#tux
# or add a shell alias: alias hms="sudo nixos-rebuild switch --flake ~/Projects/nix/nixconfigs#tux"
One-time setup — logiops
NixOS (tux)
Fully declarative via modules/nixos/logiops.nix — the service and config are managed by NixOS.
No manual steps needed.
Arch (big-rig)
home-manager deploys ~/.config/logid.cfg from configs/logid.cfg. The logid service itself
is managed manually via pacman. Install it, then point the service at the user config:
sudo pacman -S logiops
sudo systemctl edit logid
In the editor, add:
[Service]
ExecStart=
ExecStart=/usr/bin/logid -c /home/jz/.config/logid.cfg
After that, logid will pick up config changes on the next systemctl restart logid.
One-time setup — Tailscale
NixOS (tux)
Fully declarative via modules/nixos/tailscale.nix — the tailscaled daemon and tailscale CLI
are managed by NixOS. Authenticate the machine once:
sudo tailscale up # --ssh to expose Tailscale SSH, --accept-routes for subnet routes
GUI: Tailscale has no official Linux app, so tux's home profile installs Trayscale
(pkgs.trayscale) — a GTK tray front-end that drives the same tailscaled. Its icon appears in
Waybar's tray module (already enabled). Launch trayscale (or autostart it via Hyprland) to
connect/disconnect, pick exit nodes, and copy peer IPs.
Arch (big-rig)
home-manager can't run a system daemon, so tailscaled is managed manually via pacman (same
approach as logid). Install it, enable the daemon, then authenticate:
sudo pacman -S tailscale
sudo systemctl enable --now tailscaled
sudo tailscale up # --ssh to expose Tailscale SSH, --accept-routes for subnet routes
The node reconnects automatically on every boot after the first tailscale up.
One-time setup — libvirt VM networking (CachyOS)
Docker sets the FORWARD chain's default policy to DROP and only allows traffic through its
DOCKER-USER chain. This silently blocks libvirt VMs (on virbr0) from reaching the internet,
even though libvirt's own NAT/forward rules are correct. Fixed with a systemd unit that
re-inserts the missing accept rules every time docker.service starts (the rules don't survive
a Docker restart otherwise):
printf '%s\n' \
'[Unit]' \
'Description=Allow virbr0 forwarding through Docker iptables FORWARD policy' \
'After=docker.service libvirtd.service' \
'Requires=docker.service' \
'' \
'[Service]' \
'Type=oneshot' \
'ExecStart=/bin/sh -c "/usr/bin/iptables -C DOCKER-USER -i virbr0 -j ACCEPT || /usr/bin/iptables -I DOCKER-USER -i virbr0 -j ACCEPT"' \
'ExecStart=/bin/sh -c "/usr/bin/iptables -C DOCKER-USER -o virbr0 -j ACCEPT || /usr/bin/iptables -I DOCKER-USER -o virbr0 -j ACCEPT"' \
'RemainAfterExit=yes' \
'' \
'[Install]' \
'WantedBy=multi-user.target' \
| sudo tee /etc/systemd/system/libvirt-docker-forward.service >/dev/null
sudo systemctl daemon-reload
sudo systemctl enable --now libvirt-docker-forward.service
Also ensure virsh talks to the system instance (not the empty per-user qemu:///session) —
already set via LIBVIRT_DEFAULT_URI in modules/fish.nix.
NixOS VM (nixos-vm)
nixos-vm is a dev/test VM built from the same modules/nixos/common.nix and modules/nixos/desktop.nix
as real machines, differing only in its bootloader — BIOS GRUB on a virtio disk (/dev/vda) instead of
UEFI systemd-boot. Home-manager is a NixOS module, so one command applies system + dotfiles:
sudo nixos-rebuild switch --flake ~/Projects/nix/nixconfigs#nixos-vm # or: hms
The system-level Hyprland/Wayland pieces (compositor, pipewire, polkit, tty1 autologin, fish login shell)
live in modules/nixos/desktop.nix; on tty1 login modules/fish.nix execs start-hyprland
(custom.hyprlandLaunchCmd), the launcher binary nixpkgs' hyprland package ships (sets up the
systemd user session before starting the compositor — running Hyprland directly prints a warning).
NixOS machines — the template model (nixos-template)
Every physical NixOS machine is provisioned from one generic host, nixos-template, then
"graduated" into its own hosts/<hostname>/ entry on first boot. So a single installer image
serves many machines (AMD/Intel CPUs, with or without NVIDIA), all sharing:
modules/nixos/common.nix— WiFi (wpa_supplicant), steam, bluetooth (blueman), podman (dockerCLI shim, no daemon; docker-compose as thepodman composeprovider), docker-sbx (sbx,pkgs/docker-sbx/), tuicr (review AI-generated diffs like a GitHub PR, from the terminal), openssh.- generation cleanup —
boot.loader.systemd-boot.configurationLimit = 5keeps only the last 5 system generations (older ones pruned from the boot menu and/nix/var/nix/profiles/systemon each rebuild);nix.gcruns weekly to reclaim the store space that frees up once they're pruned.nix.settings.min-free/max-free(5 GiB / 20 GiB) back that up in real time — the daemon runs GC mid-build if free space drops too low, instead of a large download (e.g. tor-browser) failing with "No space left on device" between weekly runs. modules/nixos/desktop.nix— Hyprland, pipewire, Flatpak, Thunar file manager (programs.thunar+ archive/volman plugins,gvfsfor trash/mounting,tumblerfor thumbnails), tty1 autologin, upower.sbx loginneeds a Secret Service provider (services.gnome.gnome-keyring.enable) — since tty1 autologin skips the password prompt, set the first login keyring's password to blank so it unlocks without a passphrase from then on.- declarative Flatpak via
nix-flatpak(modules/nixos/desktop.nix) — Flathub remote + Zen Browser (app.zen_browser.zen) + Bitwarden (com.bitwarden.desktop), auto-updated on rebuild. Dark mode for these needsxdg-desktop-portal-gtkin home-manager's ownxdg.portal.extraPortals(modules/home/desktop/hyprland.nix) — Hyprland's home-manager module auto-enables its own portal dir viaNIX_XDG_DESKTOP_PORTAL_DIR, which otherwise shadows the system portals and hides theSettingsinterface Flatpak apps query. Zen ships no staticxdg-downloadaccess, so downloads otherwise fall through the documents portal into/run/user/$UID/doc/…instead of~/Downloads; aservices.flatpak.overridesentry grantsxdg-download:rwso files land in~/Downloads. - kanata keyboard remapper (
modules/nixos/kanata.nix, imported per-host) — GACS home row mods + Caps Lock as tap-Esc/hold-Ctrl; config inconfigs/kanata.kbd(override per-host viacustom.kanataConfig) - Tailscale mesh VPN — on NixOS declarative via
modules/nixos/tailscale.nix(tailscaleddaemon + CLI); on Arch (big-rig) managed via pacman likelogid. Authenticate a machine once withsudo tailscale up; it reconnects on boot. See "One-time setup — Tailscale". - per-host home profile (
hosts/<hostname>/home.nix) — importsmodules/home/default.nixandmodules/home/desktop; host-specific packages added inline - hyprsunset blue-light filter (started by
hyprland.lua, scheduled inmodules/home/desktop/hyprland.nix→hyprsunset.conf): identity by day, 4000 K from 20:30 - XDG user dirs (
modules/home/desktop/default.nix→xdg.userDirs) — writes~/.config/user-dirs.dirswith the standard paths and creates the folders, soXDG_DOWNLOAD_DIR(~/Downloads, where the Zen Flatpak lands downloads) etc. are explicit rather than relying on the spec fallback - home-manager wired as a NixOS module, so
nixos-install/nixos-rebuildapplies system and dotfiles together (hmsrunsnixos-rebuild switchon NixOS hosts)
Hardware differences are handled per-host: CPU microcode (AMD/Intel) is written into each
machine's hardware-configuration.nix by nixos-generate-config. NVIDIA env vars live in the
host's home.nix via custom.hyprlandExtraLua. NixOS hosts are listed explicitly in flake.nix
(nixosHosts); add a new machine by creating hosts/<name>/ and appending its name there.
Install → graduate
- Boot the installer ISO and run
sudo bootstrap. It installsnixos-template, asks for this machine's hostname, and drops~/finish-setup.sh(with the hostname baked in). First boot already has your dotfiles (home-manager is a NixOS module). - Log in as
jzand run:
It clones the repo to./finish-setup.sh~/Projects/nix/nixconfigs, createshosts/<hostname>/— the realhardware-configuration.nixfrom/etc/nixos/plus a generateddefault.nix— commits it, thennixos-rebuild switch --flake ~/Projects/nix/nixconfigs#<hostname>. - Review and
git pushwhen happy.
From then on manage the machine with sudo nixos-rebuild switch --flake ~/Projects/nix/nixconfigs#<hostname>
(or hms). The nixos-template install artifact under /etc/nixconfig is leftover and can be
deleted.
hosts/nixos-template/hardware-configuration.nixis a placeholder; each real host gets the genuine one viafinish-setup.sh(flakes only see git-tracked files, so it's committed). For a BIOS machine, swap theboot.loader.systemd-boot/efilines forboot.loader.grub.{enable,device}in the generated host config.
Tablet hosts (latitude-7212)
Dell Latitude 7212 Rugged Extreme — an Intel Kaby Lake (i5-7300U) tablet used touch-first
(optional Bluetooth keyboard). All hardware is mainline (i915, iwlwifi 8265, btusb, Atmel maXTouch,
snd_hda); the single M.2 slot takes SATA or NVMe, so regenerate hardware-configuration.nix on
the machine after the SSD is installed. Unlike the other NixOS hosts it runs GNOME, not Hyprland
— GNOME has the most mature touch story (auto on-screen keyboard, gestures, native rotation):
- Intel, not AMD —
kvm-intel+hardware.cpu.intel.updateMicrocode(auto fromnixos-generate-config); does not importnpu.nix(AMD XDNA2-only, would pinlinuxPackages_latest). modules/nixos/gnome.nix(in place ofdesktop.nix) — GNOME on Wayland via GDM, plus the compositor-agnostic desktop services (audio, portals, fonts, Flatpak, keyring) and the same GUI-app Flatpak set as tux (Zen, Bitwarden, Discord, Signal, Betterbird, OnlyOffice, Flatseal). Self-contained so the tux/Hyprlanddesktop.nixis untouched. Autologin is on (unencrypted install). GNOME's built-in OSK auto-appears in text fields — no wvkbd/hardware-button plumbing needed.modules/nixos/tablet.nix— just the iio accelerometer (GNOME reads it for native auto-rotation) andbrightnessctl. The old wvkbd + P1-buttontriggerhappytoggle andmodules/home/touch.nixwere removed when the host moved to GNOME (its OSK replaces them).cosmic.nixwas deleted.hosts/latitude-7212/home.nix— mirrors tux's software minus the Hyprland/waybar-specific bits and the terminal mail stack (GUI apps incl. mail come from Flatpak). Also drops several TUIs that are poor fits for a touch device —herald(terminal mail, redundant with the Flatpak GUI clients),aria2tui,bookokrat(terminal e-reader),eilmeldung(news ticker) — which also trims what the tablet builds from source (herald/bookokrat/eilmeldung have no binary cache). Sets GNOME's on-screen keyboard on and a dark UI viadconf.settings.modules/nixos/nvme-stable.nix— the machine's WD Black NVMe SSD hard-freezes under APST (Autonomous Power State Transition): the drive enters its deepest sleep state and never wakes, so the next I/O times out and the box hangs (nvme … I/O … timeout/controller … resetin the log; observed even while idle at a shell). It setsnvme_core.default_ps_max_latency_usvia the optionstabilityWorkarounds.nvme.apstMaxLatencyUsand is shared — imported by this host and by both installer ISOs (installer/iso.nix,installer/unattended.nix), because the live installer runs on the same WD Black and froze on it mid-install. The value defaults to0(APST fully off — the installers keep this, since a one-shot install can't roll back); the host relaxes it to5500, keeping the shallow battery-saving states and forbidding only the deepest, hanging one. Distinct from thepcie_aspm=offiniwlwifi-stable.nix— that is PCIe-link power management, whereas APST is the drive's own internal power-state machine.fwupd(for an eventual WD firmware fix via LVFS) is enabled on the host itself, not in the module, to keep it off the ISOs.modules/nixos/thermald.nix— with APST fixed, the install got far enough to surface a second freeze:mce: [Hardware Error]: Machine check events logged, a hard lock under the install's sustained CPU load (this fanless rugged tablet's Kaby Lake overheats and machine-checks).thermald(Intel Thermal Daemon) throttles the CPU proactively to keep it below its fault point. Intel-only and idle on other hardware, so it's shared by this host and both installer ISOs (the live installer cooked the CPU too). Note this only mitigates thermal MCEs — if machine-checks persist, suspect cooling (dust/thermal paste) or failing RAM/CPU (decode viapstore+ the sampler's temps).modules/nixos/no-suspend.nix— this tablet cannot resume from suspend, so suspend is disabled outright. Exhaustively confirmed on-device: unders2idlethe power button never wakes it (s2idle resumes on a live IRQ, but/proc/acpi/wakeuparms the power buttonPBTNonly for S3), and underdeep/S3 it is equally dead — aftersystemctl suspendthe machine is completely unreachable over Tailscale (via power and Home button), i.e. genuinely hung, not just a dark panel. No wake path exists in any state. The module masks the sleep targets (systemd.targets.{sleep,suspend, hibernate,hybrid-sleep}.enable = false) so nothing — manualsystemctl suspend, the GNOME power menu, idle timeout, or critical battery — can suspend the box into a hard-reset; they fail cleanly instead. The host dconf also sets idle auto-suspend tonothingso GNOME never attempts it. The screen still blanks for battery saving. Host-only (not on the installer ISOs — they never suspend). If a firmware/kernel update ever fixes resume, drop the module and revisitmem_sleep_default.modules/nixos/display-stable.nix— a black screen isn't proof of a freeze. Two i915/console causes leave the machine running with the panel dark: the Linux VT blanks after ~10 min of no keyboard input (consoleblank, default 600 s — and an unattended install has no input, so it goes dark mid-run while still installing), and Kaby Lake i915 Panel Self Refresh / framebuffer compression can spontaneously black the display. Always setsconsoleblank=0(needed by the installer VT, a no-op on the Wayland host); the installer also runssetterm --blank 0 --powerdown 0. The i915enable_psr=0 enable_fbc=0params are gated behindstabilityWorkarounds.display.disableI915Psr(default true → PSR/FBC off everywhere). Re-enabling them on the host was tried for panel battery but froze the GNOME session a few seconds after login (PSR holds a stale self-refreshed frame on this Kaby Lake panel → the desktop looks hung), so the host keeps the default and PSR/FBC stay off. Shared by the host and both installer ISOs. Diagnostic tip: if the screen goes black, don't power off — press Caps Lock (LED toggles → kernel alive → display issue, not a freeze) and wait; the install may finish blind.modules/nixos/tlp.nix— battery tuning. The host previously had zero CPU/PCIe power management (onlythermald, which throttles reactively near the thermal trip point, not proactively for battery). Enables TLP (powersavegovernor +power/balance_powerEPP, turbo boost off on battery, USB autosuspend, audio power-save) and disablespower-profiles-daemon(GNOME'smkDefault true, but it and TLP share the same sysfs knobs and can't run together — TLP's finer-grained tuning wins over ppd's quick-settings toggle). Deliberately leaves PCIe ASPM and NVMe APST untouched — this host already fought hard-freezes on both (iwlwifi-stable.nix,nvme-stable.nix) and forces wifipower_saveoff, so TLP's wifi/PCIe knobs are explicitly set to match those workarounds instead of fighting them.hosts/latitude-7212/home.nixdconf pairs with this:enable-animations = false(cuts shell-compositor GPU work) and a shorteridle-delay(120 s, down from GNOME's 300 s default) plusidle-dim— the screen panel is the single largest power draw on this tablet, larger than CPU idle draw, and nothing here can suspend anyway (seeno-suspend.nix) so a short blank timeout costs nothing but a tap to wake.
Battery tuning (tux)
modules/nixos/tlp-tux.nix — tux is an AMD Ryzen AI laptop (XDNA2 NPU, see npu.nix) that
previously had zero CPU/PCIe power management, unlike latitude-7212's tlp.nix. Enables TLP
(powersave governor + balance_performance/power EPP, turbo boost off on battery, AMD PMF
platform_profile set to low-power on battery where the firmware exposes it, USB autosuspend,
audio power-save) and disables power-profiles-daemon — mostly insurance, since tux runs Hyprland
(desktop.nix), which never sets ppd's mkDefault true the way gnome.nix does for
latitude-7212. Wifi power_save is forced off on both AC and battery to match the existing
suspend/resume workaround in common.nix rather than fight it. Unlike latitude, tux hasn't fought
any PCIe/NVMe stability bugs, so PCIE_ASPM_ON_BAT = "powersave" is enabled (AC left at platform
default) — if wifi or NVMe gets flaky on battery after this, that's the first setting to drop.
Custom installer ISO (installer)
Instead of the manual steps above, build a reusable installer ISO with this flake and a guided
bootstrap script baked in. Boot it on any UEFI machine and run one command.
Build the image (on any machine with Nix + flakes):
nix build .#installer-iso
(short alias for .#nixosConfigurations.installer.config.system.build.isoImage.) This is an
x86_64 Linux build. The ISO lands at result/iso/nixconfig-installer.iso.
Write it to a USB stick (Linux). This erases the stick — identify the device carefully; dd to
the wrong disk is irreversible.
# 1. Find the stick by size/model (USB sticks show TRAN=usb).
# Use the whole-disk name (/dev/sdX), NOT a partition (/dev/sdX1).
lsblk -dpno NAME,SIZE,MODEL,TRAN
# 2. Unmount anything auto-mounted from it, and clear stale signatures.
sudo umount /dev/sdX?* 2>/dev/null || true
sudo wipefs -a /dev/sdX
# 3. Write and flush. (Do NOT use oflag=direct — it can drop the final partial
# block and produce a truncated ISO that boots to a bare `grub>` prompt.)
sudo dd if=result/iso/nixconfig-installer.iso of=/dev/sdX bs=4M status=progress conv=fsync
sync
# 4. Verify the stick matches the ISO byte-for-byte.
sudo cmp -n "$(stat -c%s result/iso/nixconfig-installer.iso)" \
result/iso/nixconfig-installer.iso /dev/sdX && echo "OK: stick matches ISO"
Boot the stick, then get online first — nixos-install downloads nixpkgs and all packages, so
the installer needs working internet before you run bootstrap:
# wired: automatic via DHCP — just plug in
# wifi: the ISO ships NetworkManager + redistributable firmware (incl. Intel iwlwifi)
nmcli radio wifi on
nmtui # TUI, or:
nmcli device wifi connect "<SSID>" password "<PW>"
ping -c1 nixos.org # confirm connectivity
The ISO enables hardware.enableRedistributableFirmware (see installer/iso.nix) so WiFi adapters
like Intel iwlwifi work in the live installer — the stock minimal ISO ships less firmware and
leaves such cards unavailable. (enableAllFirmware is intentionally avoided: it pulls fragile
blobs such as facetimehd-calibration that fail to build.) If nmcli device still shows the
adapter unavailable, check rfkill list (run rfkill unblock wifi) or fall back to wired.
Then run the embedded bootstrap:
sudo bootstrap [DISK] [HOST]
# examples
sudo bootstrap # prompts for disk + hostname, installs nixos-template
sudo bootstrap /dev/nvme0n1 # installs the template onto that disk
sudo bootstrap /dev/sda nixos-vm # different disk + a specific host
bootstrap (defined in installer/bootstrap.sh) first verifies it can reach cache.nixos.org
(refusing to touch any disk if you're offline — override with SKIP_NET_CHECK=1), confirms before
wiping, partitions the disk
UEFI/GPT into ESP + LUKS root (200 GiB) + LUKS /home (rest) — a separate encrypted /home
so you can reinstall the OS later without losing it. Root and /home get the same passphrase
and the systemd initrd unlocks both with one prompt; nixos-generate-config captures both
boot.initrd.luks.devices entries and the /home mount. Tunables: ENCRYPT=0 (no encryption),
SEPARATE_HOME=0 (put /home on /), ROOT_END=<size> (root size, default 200GiB).
Reinstalls keep your data: on a disk that already has a /home partition, bootstrap
detects it and asks whether to keep /home or reformat the whole disk (or force it with
KEEP_HOME=1). Keeping it reformats only root + ESP and re-uses the existing encrypted /home
untouched. It then copies the embedded
flake to /mnt/etc/nixconfig, drops the generated hardware config into hosts/<HOST>/, and runs
nixos-install. It also asks for this machine's hostname and drops a ~/finish-setup.sh
(hostname baked in) that graduates the template into a real hosts/<hostname>/ on first boot (see
the template model). After install it prompts
to set the login user's password, generate an ed25519 SSH key for them, and reboot (all optional;
the user defaults to jz, override with the USERNAME env var). On an encrypted machine you'll be
prompted for the LUKS passphrase at every boot, before systemd-boot hands off to the kernel. The flake is shipped read-only inside the ISO at /etc/nixconfig (see
installer/iso.nix), so the installer works fully offline for evaluation (package downloads still
need network).
Both tux and latitude-7212 (hosts/<name>/default.nix) enable boot.plymouth.enable (default
spinner theme) plus quiet + consoleLogLevel = 3, so that bare LUKS prompt becomes a themed
graphical splash with masked password entry. Relies on boot.initrd.systemd.enable from
common.nix for clean systemd-initrd integration; Esc at boot still drops to verbose text if a boot
ever needs debugging. Not applied to nixos-template/nixos-vm — the installer flow benefits from
seeing raw boot text.
The ISO is reusable across machines and hosts — HOST defaults to nixos-template but accepts any
host under hosts/. Because the ISO embeds a snapshot of the flake, rebuild it after changing
your config to pick up the changes.
After install, log in as jz, apply the home-manager profile, and commit the generated hardware
config back to the repo (same final two steps as the manual install above).
Fully unattended installer ISO (installer-unattended)
A zero-interaction variant for imaging a machine (currently hard-wired to install host
latitude-7212). Boot it and walk away: it auto-connects a baked-in wifi, waits for
internet, picks the largest internal disk (removable media — the USB stick — is excluded), gives
a 20-second keypress-to-abort window on tty1, then wipes and installs. No encryption, so the
only baked secret is the wifi PSK (plus an optional login-password hash).
⚠️ The stick becomes a secret. The wifi PSK (and any login hash) are baked into the image in plaintext. Anyone with the stick can read them. Keep them out of git via the git-ignored secrets file below, and treat the physical stick accordingly.
1. Create the secrets file (git-ignored — see .gitignore; may live anywhere):
cp installer/unattended-secrets.example.nix installer/unattended-secrets.nix
$EDITOR installer/unattended-secrets.nix # wifiSSID, wifiPSK, optional userPasswordHash
# hash for sudo (optional): nix run nixpkgs#mkpasswd -- -m sha-512
The secrets are read impurely from $NIXCFG_UNATTENDED_SECRETS — never from the flake source —
so they stay out of git entirely. In pure eval (nix flake check, the security-scan CI) that env var
is empty, so the .#installer-unattended-iso output simply doesn't exist and nothing else is affected.
2. Build and flash (same dd procedure as the interactive ISO above). The build needs --impure
and the env var pointing at your secrets file — the justfile wraps that:
just installer-unattended # wraps the --impure + env-var build; guards a missing secrets file
# or point at a secrets file elsewhere:
just secrets=/run/keys/wifi.nix installer-unattended
# equivalent raw command:
NIXCFG_UNATTENDED_SECRETS="$PWD/installer/unattended-secrets.nix" \
nix build --impure .#installer-unattended-iso
# result/iso/nixconfig-installer-unattended.iso → dd to the stick
3. Boot the target. It runs installer/unattended-install.sh as unattended-install.service
on tty1: connectivity check → largest-internal-disk pick → 20 s abort window → GPT (ESP + ext4
root) → nixos-generate-config → embedded flake copied to /mnt/etc/nixconfig with the generated
hardware config injected into hosts/latitude-7212/ → nixos-install --no-root-passwd → optional
chpasswd from the baked hash → reboot. A /etc/unattended/done marker + a service
ConditionPathExists guard stop it re-running.
Safety notes: the disk picker filters on RM==0 so it can only target non-removable disks (never
the boot stick); if nothing qualifies it aborts rather than wiping anything. To retarget a different
host or partition scheme, edit installer/unattended.nix (service environment) and
installer/unattended-install.sh. Since it's baked from a flake snapshot, rebuild the ISO after
changing your config. Test it in a VM before trusting it on real hardware.
Debugging a stall/freeze. The install script is instrumented so a hang leaves evidence instead of a blank screen:
- A diagnostics snapshot (
diag_dump) prints at start and on failure — RAM/swap, disks, network + wifi driver, recentiwlwifi/MCEdmesglines, CPU temps, and any prior-boot crash log frompstore. - All output is mirrored to a logfile, and once the target disk is mounted a background sampler
writes a state snapshot (RAM, temps, last
iwlwifi/MCE/OOM line, store size) every 5 s to the target disk (sync'd), so a hard freeze mid-download leaves a breadcrumb trail. Everything lands in/var/log/unattended/(install.log,samples.log,dmesg.txt) — which survives a reboot and persists onto the installed system. After a freeze, reboot the live stick,mountthe root partition, and read those files. - Mirror logs onto the boot USB stick (easiest to retrieve). The ISO filesystem is read-only, so
add a spare FAT32 partition labelled
NIXLOGto the stick's free space once (after writing the ISO). When present, the installer mounts it (-o sync) and mirrorsinstall.log+samples.log(+dmesg.txton failure) into/unattended/on it — from the very first line, independent of the target disk, so even an early "no internet" abort or a download-phase freeze is captured. Then just pull the stick, plug it into any machine, and open the plain-text logs (no need to boot the target or pull its internal disk). The easiest path isjust flash [disk](default/dev/sda), which builds the unattended ISO,dds it to the stick, and adds theNIXLOGpartition in one step (it refuses any non-removable disk and makes you confirm). To prep an already-flashed stick by hand instead (usesfdisk, notsgdisk— gptfdisk can't parse the ISO's isohybrid GPT and dies with "Invalid partition data!"):
(Label matching is set by# /dev/sdX is the boot stick. The ISO occupies partition(s) at the front; grow # the GPT to fill the stick, append ONE partition in the freed space, FAT32 it. sudo sfdisk --relocate gpt-bak-std /dev/sdX # move backup GPT to the true disk end printf ',,L\n' | sudo sfdisk --append /dev/sdX # append a partition spanning the free space sudo mkfs.vfat -F 32 -n NIXLOG /dev/sdXN # N = the new partition numberUSB_LOG_LABELinunattended.nix's service env. No stick partition → logging is unchanged. The stick already holds the baked wifi PSK, so it's a secret either way; the password hash is kept out of the log.) nixos-installruns verbose with--option stalled-download-timeout 90 --option connect-timeout 15, so a stalled fetch fails loudly (hitting the error trap + debug shell) instead of hanging forever.DEBUG=1(default inunattended.nix's service env) adds a timestamped per-command trace; set it to"0"to quiet things once the machine installs cleanly.boot.consoleLogLevel = 7surfaces kernel messages (firmware crashes, MCE, OOM) on the console.- Swap (the tablet is low-RAM). The live installer ships no swap, and the download/unpack phase ran
this tablet out of memory (~130 MiB free, thrashing to a crawl — which looked like a hardware freeze
for a long time: downloads stall, new gettys won't spawn, the box is unresponsive but the kernel is
still alive). Fixed two ways:
zramSwap.enable(compressed in-RAM swap, from early boot, both ISOs) and a real swapfile the install script puts on the mounted target (SWAP_MB, set to 16384 inunattended.nixsince the config compiles several non-cached Rust/Go packages that peak multi-GB on the 8 GB tablet; script fallback 8192; removed before reboot). The installed host useszramSwapat runtime too (GNOME on low RAM). If a future install still starves, raiseSWAP_MBfurther.
Intel 8265 wifi (modules/nixos/iwlwifi-stable.nix). The Latitude's Intel 8265 is served only by
iwlwifi (no alternative driver exists to switch to), and its firmware crashes / stalls downloads
under power management and Tx aggregation — the root cause behind the "stuck downloading" freezes. The
module has two profiles via stabilityWorkarounds.iwlwifi.profile. "aggressive" (default, used by
both installer ISOs) throws everything at the firmware to guarantee a one-shot install completes:
power_save=0, bt_coex_active=0, swcrypto=1, 11n_disable=8, uapsd_disable=1, power_scheme=1
plus a global pcie_aspm=off. "relaxed" (the latitude-7212 host) keeps only the cheap
power-save options (power_save=0, uapsd_disable=1, power_scheme=1) and restores the throughput /
battery / Bluetooth-coexistence features the install-time hammers gave up — BT coexistence matters
because this tablet's only keyboard is Bluetooth, and dropping the global pcie_aspm=off reclaims idle
battery (the NVMe freeze is APST, not PCIe ASPM, so it isn't reintroduced). The host can roll back if
the relaxed profile stalls. If the installer still stalls on "aggressive", escalate 11n_disable=8 → 11n_disable=1 (disables 802.11n entirely: slower, most stable). Only add iwlwifi params that
modinfo -p iwlwifi lists — an unknown option makes the module fail to load and wifi vanish.
Nitrokey SSH (OpenPGP smartcard)
Nitrokey SSH is enabled by importing modules/nixos/nitrokey.nix (currently on tux and the
latitude-7212 tablet): hardware.nitrokey.enable (udev), services.pcscd.enable (smartcard
daemon), and programs.gnupg.agent with enableSSHSupport — so gpg-agent is the SSH agent and
the card's authentication subkey signs SSH challenges. modules/fish.nix already points
SSH_AUTH_SOCK at gpg-agent's socket. The PIN prompt is the customNitrokey.pinentryPackage option:
terminal pinentry-curses by default (tux); the touch tablet overrides it to pinentry-gnome3 so the
PIN dialog is usable by touch and triggers the on-screen keyboard. Using a second Nitrokey on
another host just needs the same auth subkey provisioned onto it (below) and, if it's a different key,
its gpg --export-ssh-key public key added to the git server.
You only ever export the public key; the private keys never leave the card.
Provisioning a key on a Nitrokey 3 (OpenPGP applet)
Always back up the secret key (step 3) before
keytocard— it's a move. Losing the backup (and having no fetch URL / keyserver copy) means GPG can never use the card keys again.
# 1. Reset the OpenPGP applet (wipes old keys, unblocks PINs; other applets untouched)
gpg --card-edit → admin → factory-reset # defaults: user 123456 / admin 12345678
# 2. Generate a certify-only primary + S/E/A subkeys (RSA 4096)
gpg --expert --full-generate-key # (8) RSA set-own-caps -> Certify only, 4096
gpg --expert --edit-key <email> # addkey ×3: sign-only, encrypt-only, auth-only; save
# 3. BACK UP FIRST (offline!)
gpg --export-secret-keys --armor <email> > gpg-secret-backup.asc
gpg --export --armor <email> > gpg-public.asc
# 4. Move subkeys to the card
gpg --edit-key <email> # key 1/2/3 -> keytocard (Sig/Enc/Auth); save
# 5. Set a fetch URL (host gpg-public.asc somewhere) + change PINs
gpg --edit-card → admin → url / passwd
# 6. Export the SSH public key for your VCS / git server
gpg --export-ssh-key <email> > nitrokey.pub
Using it on another machine
Insert the card, then get the public key into that keyring:
gpg --edit-card → fetch # if a URL is set (step 5); else: gpg --import gpg-public.asc
gpg --card-status # "General key info" now shows your key
ssh-add -L # card's SSH key, served by gpg-agent
Optional — sign commits with it: git config --global commit.gpgsign true and
git config --global user.signingkey <primary-keyid>.
Recovering an SSH pubkey when the GPG certificate is lost but the card still has the keys: read it straight off the card over PKCS#11 —
gpgconf --kill scdaemon; ssh-keygen -D opensc-pkcs11.so.
Mail — local-first (mbsync + notmuch + aerc/meli) + KMail/Claws Mail/Betterbird
modules/mail.nix wires up a local-first mail stack to test against GUI clients:
mbsync (isync) pulls IMAP down to ~/Mail/<account> on a 5-minute systemd timer, notmuch
indexes it (Xapian full-text search, scales to huge mailboxes without the lag Betterbird hit),
and aerc/meli are terminal clients on top. kdePackages.kmail is installed as a GUI
alternative (home.nix); Claws Mail and Betterbird are installed declaratively via Flatpak
(modules/nixos/desktop.nix → services.flatpak.packages). The waybar bar's leftmost
right-side module (custom/mail, configs/waybar/mail.sh) shows the total
notmuch count tag:unread across all accounts; clicking it opens aerc in a terminal.
Credentials come from a self-hosted Vaultwarden instance via rbw (an unofficial but scriptable
CLI — unlike the official bw, it keeps an unlocked vault behind a background agent, so
passwordCommand doesn't need to prompt on every sync). programs.rbw.settings.base_url in
modules/mail.nix points at that instance, so rbw never talks to bitwarden.com.
Accounts (accounts.email.accounts in modules/mail.nix): julius@zeidlos.com (Gmail/Google
Workspace, primary) plus several kasserver.com-hosted IMAP addresses
(jz@chaos.li, banking@zeidlos.de, do7jz@chaos.li). Each account's passwordCommand pulls
its password from a matching Bitwarden item — for Gmail, that item must hold an App Password
(requires 2FA), not the account's normal login password.
One-time setup:
rbw loginandrbw unlockonce, interactively (usesbase_url/emailalready set inmodules/mail.nix, so norbw config setneeded first).sudo nixos-rebuild switch --flake ~/Projects/nix/nixconfigs#tux, then runmbsync --allonce by hand to do the initial sync (the systemd timer only fires every 5 minutes after that).
mbsync's remove/expunge default to none, so nothing propagates deletions in either
direction while this is just a test setup.
Adding another account: scripts/gen-mail-accounts.sh reads a Bitwarden folder (default
email) via rbw and prints ready-to-paste accounts.email.accounts.<name> blocks matching
the ones already in modules/mail.nix — it assumes item names look like <address>-password
/ <address>-app-password, that the item's Bitwarden username field holds the provider
login (e.g. a kasserver.com account number, not the email address), and that every non-Gmail
domain shares one IMAP/SMTP host. No password ever appears in its output; review the printed
block before pasting it in.
rbw unlock
nix shell nixpkgs#jq nixpkgs#rbw -c ./scripts/gen-mail-accounts.sh
Brave — de-Googled, privacy-first Chromium (with Web Bluetooth)
A Chromium browser for the sites Zen/Firefox can't handle, stripped of the Google/crypto/AI
bloat. Brave is installed per-user via home-manager (modules/home/desktop/brave.nix →
programs.brave); the debloat is enforced by a locked managed policy
(configs/brave-policies.json) that force-disables Rewards, Wallet, VPN, Leo AI, News, Talk and
Tor, plus all telemetry (P3A, stats ping, Web Discovery, metrics reporting) and Sync/Safe
Browsing/autofill. Policy keys set there are greyed out in settings — verify at brave://policy.
Chromium reads managed policies from /etc/brave/policies/managed/*.json:
- NixOS: shipped declaratively by
modules/nixos/brave.nix(environment.etc), imported viamodules/nixos/desktop.nixso every NixOS desktop host gets it. - Arch (big-rig): no
environment.etc, so copy it by hand (mirrors logiops):sudo install -Dm644 configs/brave-policies.json /etc/brave/policies/managed/brave.json.
Web Bluetooth is off by default in Linux Chromium, so programs.brave.commandLineArgs launches
Brave with --enable-experimental-web-platform-features; BlueZ is already enabled system-wide
(modules/nixos/common.nix). That switch turns on all experimental web-platform features — if
--enable-features=WebBluetooth alone suffices on your Brave build, prefer the narrower flag.
Neovim config
The LazyVim config lives at configs/nvim/ in this repo. modules/neovim.nix
points ~/.config/nvim at it via an out-of-store symlink (not a store copy),
so LazyVim can write lazy-lock.json back into the repo on :Lazy update/sync/clean — commit that file's changes like any other edit.
Claude Code — nixos MCP server
mcp-nixos is a read-only MCP server that lets Claude Code query NixOS packages/options, Home
Manager, nix-darwin, flakes, and package version history. It's registered project-scoped in
.mcp.json, launched via uv so it also works in sandboxes/containers that don't have the
home-manager profile (the PyPI package is fetched on demand):
{ "mcpServers": { "nixos": { "command": "uv", "args": ["tool", "run", "mcp-nixos"] } } }
(uv tool run is the long form of uvx; the Nix package pkgs.mcp-nixos in
modules/home/default.nix still provides the CLI directly on hosts.) On first launch in this repo,
Claude Code prompts to approve the server; use /mcp to reconnect after config changes. It needs
outbound network (PyPI on first run, then search.nixos.org, FlakeHub, noogle.dev, cache.nixos.org)
at runtime.
Known limitations / things still managed by pacman
| Package | Reason |
|---|---|
zen-browser |
Not in nixpkgs — on Arch use flatpak/AUR; NixOS hosts get it via declarative Flatpak (modules/nixos/desktop.nix → services.flatpak.packages) |
bitwarden-desktop |
Native nixpkgs build currently pulls EOL electron — NixOS hosts get it via declarative Flatpak (modules/nixos/desktop.nix) |
betterbird |
Not in nixpkgs — on Arch use flatpak/AUR; NixOS hosts get it via declarative Flatpak (modules/nixos/desktop.nix) |
hyprlock |
Nix build links against NixOS PAM path — use AUR |
cachyos-fish-config |
CachyOS-specific — keep via pacman |
keyd |
Needs system-level access — keep via pacman + systemd |
krew |
kubectl plugin manager — install manually |
virtualbox-host-dkms |
Kernel modules must be built against the running kernel — Nix can only install the userspace virtualbox binary; CachyOS uses a custom kernel so DKMS is required: sudo pacman -S virtualbox-host-dkms linux-cachyos-headers && sudo modprobe vboxdrv |
System-level packages (kernel, drivers, firmware) always stay with CachyOS pacman.
Ham radio
Amateur-radio software is bundled in one reusable home-manager module,
modules/home/ham-radio.nix, imported by every host that carries a radio — currently
tux (shack) and the rugged latitude-7212 tablet (field). Both get the identical set;
edit the list once in the module rather than per host. It's a "Ham Radio Linux"-style
toolkit grouped by function:
| Function | Tools |
|---|---|
| Weak-signal / keyboard digital | js8call, wsjtx (FT8/FT4/WSPR), fldigi + flrig, gridtracker |
| APRS / packet | direwolf (soundcard TNC), xastir (map client), aprx (iGate/digipeater), multimon-ng, ax25-tools, ax25-apps |
| Rig control | hamlib (rigctl/rigctld), wfview (GUI control/waterfall for modern Icom rigs) |
| Logging / QSL | qlog, tqsl (LoTW) |
| Winlink | pat (email over radio) |
| Satellites | gpredict |
| Image / CW | qsstv (SSTV), aldo (CW trainer) |
| SDR receive / DSP | gqrx, sdrangel, cubicsdr, gnuradio (GRC flowgraphs) |
| Radio programming | chirp (analog), qdmr (DMR — see the qdmr overlay note below) |
Everything is a plain user package — no system services are enabled by default. USB-serial
CAT/rig control and radio programming work without root because jz is in the dialout group
(modules/nixos/common.nix). direwolf/aprx are installed as CLI tools; run them by hand per
session, or promote to a NixOS service later if you want a persistent iGate/digipeater. qdmr
resolves to the 0.15.1 overlay (overlays/qdmr.nix) until the nixpkgs PR lands. ax25-tools
needs overlays/ax25-tools.nix to build at all: its hdlcutil/ subdir (sethdlc/smdiag/smmixer,
config tools for the long-removed hdlcdrv/baycom/soundmodem kernel drivers) includes a kernel
header, linux/hdlcdrv.h, that current nixpkgs headers no longer ship — the overlay drops that
subdir from the build (dead code for hardware no modern kernel drives anyway) rather than vendor
the removed header; drop the overlay once nixpkgs' ax25-tools does the same upstream.
The NixOS-side counterpart, modules/nixos/ham-radio.nix (imported by the same two hosts), sets
hardware.hackrf.enable = true and puts jz in plugdev so HackRF-family USB SDRs — including
the rad1o badge, which enumerates as a HackRF (1d50:6089) — work in gqrx/hackrf_info without
root. Without it the device node stays root-owned and gqrx fails with a permissions error.
angryoxideis a Wi-Fi (802.11) attack tool, not ham radio — it lives in the security module (modules/home/security.nix), not this one.
Embedded/firmware dev toolchain
modules/home/embedded-dev.nix, imported by tux and big-rig: gcc-arm-embedded
(arm-none-eabi-gcc cross toolchain for Cortex-M/R), gnumake, cmake, tinyxxd (standalone
xxd — hex dump / xxd -i C-array conversion — without pulling in vim), and a python3 with
pyyaml importable. Generic ARM firmware build toolchain, not tied to any one project.
Security tooling
Two home-manager modules carry the DevSecOps / security-research toolkit. The base image
(modules/home/default.nix, every host) already ships grype/trivy/syft for SCA/SBOM; these
modules layer on the rest. RF signal research is split from appsec so the two can be imported
independently — and they are: the heavy pentest suite (security.nix) is imported on tux
only (kept off the low-RAM latitude-7212 tablet), while the RF/SDR field kit (rf-sec.nix)
rides along with the SDR gear on both tux and latitude-7212.
modules/home/security.nix — DevSecOps + pentest (tux only):
| Area | Tools |
|---|---|
| Secrets | trufflehog, gitleaks |
| IaC / misconfig | trivy config (base image); checkov dropped — hard dep on insecure ecdsa |
| Supply-chain signing | cosign (Sigstore) |
| Vuln / dependency scan | vulnix, osv-scanner |
| Cloud & K8s | kubescape, kube-bench, cloudhunter, prowler, scoutsuite, pacu (AWS) |
| Exploitation frameworks | metasploit; routersploit dropped — unmaintained, imports pkg_resources (removed in Python 3.14) |
| Passwords / hashes | hashcat, john, thc-hydra |
| Recon / attack surface | amass, subfinder, dnsx, naabu, httpx, katana |
| Web / DAST | nuclei (+nuclei-templates), nikto, ffuf, feroxbuster, sqlmap, testssl |
| Network & traffic | nmap, zmap, wireshark, netsniff-ng, dsniff, bettercap, mitmproxy (msgpack-pin relaxed via overlays/mitmproxy.nix), snort, ssh-audit |
| Wireless | angryoxide, aircrack-ng (Wi-Fi 802.11) |
| Tunneling / anonymity | iodine (IP-over-DNS), tor-browser |
modules/home/rf-sec.nix — RF/SDR signal research (the analysis layer above the plain SDR
receivers in ham-radio.nix): urh (Universal Radio Hacker — demod/decode/fuzz), inspectrum
(offline signal analysis), rtl_433 (ISM device decode), kalibrate-rtl (ppm calibration),
soapysdr (SDR abstraction). Hardware-specific host tools (hackrf, airspy, ubertooth,
killerbee, proxmark3) are listed commented-out in the module — uncomment per the gear you own.
Live capture (
wireshark), and rawnmap/bettercapmodes, need elevated privileges. Run them undersudo, or enable the NixOSprograms.wiresharkmodule for a setcap'ddumpcapif you want non-root capture.
Custom packages — flake outputs
All packages under pkgs/ are exposed as top-level flake outputs for direct builds and CI:
nix build .#aria2tui
nix build .#bookokrat # resolves to upstream flake input (github:bugzmanov/bookokrat)
nix build .#eilmeldung
nix build .#herald
nix build .#nmrs
bookokrat and eilmeldung come from upstream flake inputs — update them with
nix flake update bookokrat eilmeldung. No local packages. bookokrat currently needs a
one-line postPatch (in both flake.nix's packages.bookokrat and hosts/tux/home.nix's
inline overrideAttrs, since they're separate overrides of the same input): upstream's
src/clipboard.rs fn detect() -> Provider has a #[cfg(not(target_os = "android"))] block that
falls off the end without a return, a genuine compile break on any non-android target (not a Nix
issue) — same fix rustc itself suggests. Drop the postPatch once upstream fixes it.
herald is a source build (Go). On first build after a version bump, set both
src.hash and vendorHash to lib.fakeHash in sequence, resolving each from the build error.
Supply chain security
CI workflows (.forgejo/workflows/)
| Workflow | Trigger | What it does |
|---|---|---|
security-scan.yml |
push to main, weekly Monday 06:00 UTC |
Builds all custom packages; runs vulnix (NVD/OSV CVE scan) and grype (fail on HIGH); generates CycloneDX SBOMs uploaded as 90-day CI artifacts |
update-flake.yml |
weekly Monday 04:00 UTC | Runs nix flake update, builds nixosConfigurations.tux to verify, opens a PR with a table of updated input revisions |
The update workflow requires a FORGEJO_TOKEN secret (repo → Settings → Secrets) — a personal
access token with repository write + pull-request write scope.
Ad-hoc scanning
# CVE scan a built package
OUT=$(nix build .#bookokrat --print-out-paths --no-link)
nix run nixpkgs#vulnix -- "$OUT"
# Generate a CycloneDX SBOM (syft is installed via modules/common.nix)
syft scan dir:"$OUT" --output cyclonedx-json --file bookokrat.cdx.json
Structure
nixconfig/
├── flake.nix # inputs: nixpkgs-unstable + home-manager
├── arch.nix # CachyOS/Arch (x86_64-linux): all modules + Linux packages
├── nixos-vm.nix # NixOS VM (x86_64-linux): trimmed desktop-essentials home-manager profile
├── macbook.nix # macOS (x86_64-darwin + aarch64-darwin): shared mac config
├── hosts/
│ └── nixos-vm/
│ ├── configuration.nix # NixOS system config: bootloader, networking, users
│ └── hardware-configuration.nix # generated by nixos-generate-config, machine-specific
├── modules/
│ ├── fish.nix # shell, aliases, abbreviations, functions
│ ├── git.nix # git + lazygit
│ ├── tmux.nix # tmux with nixpkgs plugins (replaces TPM)
│ ├── neovim.nix # neovim binary + LSP deps (LazyVim manages plugins)
│ ├── starship.nix # prompt
│ ├── terminal.nix # ghostty
│ ├── herdr.nix # herdr config.toml + HERDR_PROCESS_DETECTION
│ ├── waybar.nix # waybar config + style — shared by arch.nix and home.nix (Linux only)
│ ├── mail.nix # mbsync/notmuch/aerc/meli — local-first mail (see "Mail" section)
│ ├── nixos/
│ │ ├── desktop.nix # NixOS system module: hyprland, pipewire, polkit, thunar, tty1 autologin
│ │ └── npu.nix # AMD XDNA2 NPU device access (render group)
│ └── arch/
│ ├── hyprland.nix # hyprland + hyprpaper + hypridle (Linux only)
│ ├── desktop.nix # rofi, dunst, yazi, btop, xdg portals (Linux only)
│ ├── logiops.nix # MX Master 4 logiops config + systemd service (Linux only)
│ └── media.nix # mpd, ncspot (Linux only)
├── .forgejo/workflows/ # CI: security-scan.yml (vulnix/grype/sbom), update-flake.yml (weekly PR)
├── pkgs/ # custom packages not in nixpkgs, callPackage'd from modules
│ ├── adsb-tui/ # terminal ADS-B aircraft tracker, packaged from upstream release tarball
│ ├── aria2tui/ # Python TUI frontend for aria2c, packaged from PyPI
│ # bookokrat — upstream flake input (github:bugzmanov/bookokrat); no local package
│ ├── docker-sbx/ # Docker Sandboxes CLI, packaged from upstream release tarball
│ # eilmeldung — upstream flake input (github:christo-auer/eilmeldung); no local package
│ ├── herald/ # terminal email client, built from source (needs hash update on first build)
│ └── nmrs/ # waybar network on-click GUI, built from networkmanager-rs/nmrs-gui
└── configs/ # raw config files referenced by modules
├── hyprland.lua
├── hyprlock.conf
├── dunstrc
├── logid.cfg
├── kanata.kbd
├── waybar-style.css
└── waybar/
├── webcam.sh
├── timezones.sh
├── mx4-battery.sh
├── corne-battery.sh
└── mail.sh