Development hosts
Flat, explicit NixOS modules, with locked inputs. Required machine setup belongs in these configs—not a list of manual installs or dotfile copies.
Choose the host, not the login name: nixosConfigurations.nixos is this physical UEFI/NVMe machine; nixosConfigurations.dev is EC2 only. Both add the dev account. Never activate the EC2 closure on the physical machine.
| File | Owns |
|---|---|
flake.nix, flake.lock |
Host entry point and exact Nixpkgs/Home Manager/dotfile revisions and content hashes |
configuration.nix |
EC2-only boot/storage/integration, updater and repo ownership |
common.nix |
Shared dev environment, platform, Nix features and compatibility version |
physical.nix, hardware-configuration.nix |
This physical host's systemd-boot/NVMe/Btrfs setup; retains kbot, Plasma/SDDM and NetworkManager while adding dev |
physical-test.nix |
Regression checks for physical-host recovery access, storage, boot and VPN tools |
users.nix |
dev, SSH/sudo, Home Manager integration and dev workspace ownership |
dev-authorized-keys |
Public SSH keys for dev—never private keys |
tools.nix, colors.nix |
Zsh, Kitty, Pi, Starship, fzf, Yazi, btop, Git policy and common CLI tools; shared readable palette |
desktop.nix, hyprland.lua, anyrun.css, wallpaper.svg |
Hyprland/UWSM, bar, launcher, original wallpaper, notifications, lock/idle, PipeWire and desktop styling |
apps.nix |
Firefox ESR, KeePassXC, Thunderbird, Steam, Element, Slack and Zathura |
workstation.nix, nvidia.nix |
Separately selected physical-workstation/greeter and NVIDIA integration; not enabled on EC2 |
updates.nix, update-system.sh |
Daily package-input updates in an isolated Git worktree; validated commits, no forced reboot or GC |
desktop-test.nix, desktop-test.py, audit-desktop.sh, update-test.py |
Disposable graphical audit and updater failure/concurrency tests |
network.nix |
WireGuard/OpenVPN clients and network diagnostics; optional resolved, host-owned interface management |
neovim.nix, neovim-test.lua |
Editor, unchanged upstream dotfile deployment and opt-in native runtime audit |
| DESKTOP.md | Historical component research; executable configuration is in the files above |
Account
- Daily user:
dev, UID 1001, home/home/dev, workspace~/projects. - Passwordless sudo is scoped to
dev; EC2's SSM-user administration and root SSH recovery remain intact. The physical host retainskbot's existing wheel/sudo access. devis authorized by the public key in this repo. No private key or password is embedded.- On EC2 only,
/etc/nixosis writable bydevthrough a native tmpfiles ownership rule, which does not follow store symlinks. The physical target leaves the original/etc/nixosrecovery configs and/home/kbotuntouched;/etc/nixremains administrator-managed. - Nix daemon access stays untrusted for ordinary use. Sudo is a separate, explicit administrative capability.
- The declared daily shell is Zsh, with completion, suggestions, highlighting, Starship, fzf (
Ctrl-R,Ctrl-T,Alt-C), and zoxide (z,zi). Root/SSM shells are not changed. - Kitty uses an opaque dark background, 13pt JetBrains Mono and generous padding.
Ctrl-Shift-+/Ctrl-Shift--zoom its font;Ctrl-Shift-Backspaceresets it.yopens Yazi with shell-directory integration. - Desktop keys:
Super-EnterKitty,Super-Spacelauncher,Super-EYazi,Super-BFirefox,Super-PKeePassXC,Super-Escapelock,Super-Qclose,Super-1…0workspaces,Printscreenshot/annotation. Launch the Hyprland (uwsm-managed) session. - KeePassXC preferences are seeded once and remain writable. Open/create your own vault and pair its browser extension; no vault, account or VPN credentials are embedded. The optional ReGreet module (
workstation.nix, not used by either host here) requires a securely provisionedhashedPasswordFile; the physical host keeps SDDM and a locally setdevpassword. EC2 gets no greeter or autologin. - Element uses libsecret for encrypted local storage through KeePassXC. Before using it, create/open your own vault and select a dedicated application-token group in Database Settings → Secret Service Integration. Keep that vault unlocked when requested. Do not select Element's weaker/no-encryption fallback. Vault setup and application authentication are intentionally user-controlled.
wg,wg-quick, OpenVPN,iperf3,nmap,traceroute,whois,mtr, DNS tools,tcpdump,ethtool,ncandsocatare installed without any tunnels, peers, keys or added firewall ports. The physical host also has NetworkManager's OpenVPN plugin and keeps its existing DNS setup. EC2 uses resolved; DHCP remains under each host's network manager.
Enter from an administrator session with sudo -iu dev.
Git author identity and remote destination are deliberately unset. user.useConfigOnly is now a Home Manager setting in tools.nix, not a required manual git config step.
Build and apply
Physical machine: keep kbot and add dev
physical.nix preserves kbot (UID 1000, /home/kbot, existing password, wheel/NetworkManager groups), KDE Plasma, SDDM, printing, locale/time zone, and the installed filesystems/boot loader. Home Manager manages only dev, UID 1001 with its own /home/dev. No autologin, password copying, or replacement greeter is enabled. The EC2 automatic updater is deliberately not imported.
The hardware file is a tracked copy of this machine's /etc/nixos/hardware-configuration.nix; do not use it unchanged on another machine. The original /etc/nixos files remain available as recovery configuration.
Recovery from the earlier EC2 switch attempt: that failure selected system-profile generation 2 even though generation 1 is still running and remains systemd-boot's default. Before testing the physical configuration, if the profile still points at the failed EC2 closure, restore its selection without activating anything:
readlink -f /nix/var/nix/profiles/system
sudo nix-env --profile /nix/var/nix/profiles/system --switch-generation 1
This recovery command is specific to the observed generations on this machine; it is not a routine rebuild step.
From an administrator shell:
cd /etc/nix
nix flake check --no-build --no-update-lock-file
nix build \
.#checks.x86_64-linux.physical-config \
.#checks.x86_64-linux.updates .#checks.x86_64-linux.desktop-config \
--no-update-lock-file --no-link
nix build \
.#nixosConfigurations.nixos.config.system.build.toplevel \
--no-update-lock-file --out-link result-nixos
built=$(readlink -f result-nixos)
sudo "$built/sw/bin/nixos-rebuild" dry-activate --no-reexec --store-path "$built"
Use result-nixos, not an old result pointing at the EC2 closure. The dry run must not remove kbot, SDDM, NetworkManager or the /boot, /home, /nix mounts. Existing services can still need a restart; keeping an account does not guarantee its current graphical session stays running.
Save your work, then activate from a text console (Ctrl-Alt-F3, log in as kbot, or use root). Keep that console available during testing:
cd /etc/nix
built=$(readlink -f result-nixos) # Re-establish this shell variable on the new console.
# Activates users/services, but does NOT change the boot default. This is not a dry run.
sudo "$built/sw/bin/nixos-rebuild" test --no-reexec --store-path "$built"
# Choose a new password locally; mutable users preserve it across later rebuilds.
sudo passwd dev
At SDDM, choose dev → Hyprland (uwsm-managed) to try the new desktop, or kbot → Plasma for the existing environment. Once satisfied, persist the exact tested closure:
sudo "$built/sw/bin/nixos-rebuild" switch --no-reexec --store-path "$built"
If testing fails, use the retained text console to restore the booted system without changing the boot default:
previous=$(readlink -f /run/booted-system)
sudo "$previous/sw/bin/nixos-rebuild" test --no-reexec --store-path "$previous"
If a later boot fails, select the original generation in systemd-boot (hold Space during startup). Keep recovery generations; do not garbage-collect them during migration. A working kbot account cannot recover an unbootable kernel/filesystem by itself.
EC2 only
As dev, from /etc/nixos on the EC2 host:
nixfmt --check ./*.nix
nix flake check --no-build --no-update-lock-file
nix build .#checks.x86_64-linux.updates .#checks.x86_64-linux.desktop-config --no-link
nix build .#nixosConfigurations.dev.config.system.build.toplevel --no-update-lock-file
New source files must be added to Git for flakes to see them. Keep flake.lock in version control. A build does not activate changes.
Review and activate exactly the built closure:
built=$(readlink -f result)
sudo "$built/sw/bin/nixos-rebuild" dry-activate --no-reexec --store-path "$built"
sudo "$built/sw/bin/nixos-rebuild" switch --no-reexec --store-path "$built"
Use the rebuild tool from that closure with --no-reexec: otherwise the bootstrap tool can try to rebuild itself through the old channel even when --store-path is supplied. Capturing built also keeps review and activation on the same immutable result.
For initial deployment on a compatible NixOS EC2 base where flakes are not enabled yet, check out this repo and run the build as an existing administrator with the temporary CLI flag:
nix --extra-experimental-features 'nix-command flakes' build \
.#nixosConfigurations.dev.config.system.build.toplevel --no-update-lock-file
Then review/apply the resulting closure as above. The configuration creates dev, sets permissions and deploys its files. No separate Neovim clone, copy, useradd, chown or global Git-config recipe is required. After either host configuration is active, nix-command and flakes are system defaults through nix.settings.experimental-features; the temporary flag is only for bootstrapping older bases that cannot read flakes yet.
This host build does not use a mutable channel. NixOS's native flake integration pins the nixpkgs registry entry and login-shell <nixpkgs> lookup to the system input; the global nix-path setting keeps the same pin when NIX_PATH is unset. Old root channel profiles are retained for recovery, not used as build inputs. Dev-environment templates/composition remain deferred; there is no flake framework here.
Neovim: import, do not rewrite
The input is the existing neovim-dots repository, initially pinned to 380eb86778a7c53a0f1c18e84f14037456155347.
Home Manager deploys its files under ~/.config/nvim, with the Lua, AstroNvim, Lazy, Mason, plugins and keymaps unchanged. programs.neovim.configure stays empty so Neovim discovers init.lua normally. No Nixvim or custom Lua loader.
tools.nixsupplies GCC/Make, pkg-config, Python, Node, Lua 5.1/LuaRocks andnix-ldfor the existing plugin builds and Mason's upstream Linux executables. These are runtime prerequisites, not a replacement plugin manager or project-template framework.- Configuration files are linked from the pinned source and managed by Nix. Change the upstream repo and its input revision rather than editing generated links.
- Lazy's
lazy-lock.jsonmust remain writable. The config seeds a copy at~/.local/state/nvim/locks/<dotfile-revision>.jsonand links to it. A new dotfile revision gets its own original lock; repeated activation preserves runtime changes to an existing lock. - A declared migration preserves the earlier manual checkout intact at
~/projects/neovim-dots-before-nix. It refuses to overwrite an existing backup. On a clean home this migration does nothing.
Reproducibility boundary: Nix locks the host inputs and dotfile source, and reproduces their deployment. The existing Lua still bootstraps Lazy and manages plugin/Mason downloads at runtime. The supplied Lazy lock records plugin revisions, but it is writable and Mason's tool versions are not pinned by this Nix config. This is not a claim that every runtime download/cache is a Nix-reproducible build. Changing that policy requires a separate agreement; do not silently replace the user's plugin managers.
Validation and commits
The initial deployment was tested with an empty disposable home: all upstream files were reproduced byte-for-byte, the Lazy lock remained writable across repeated activation, and the migration preserved local data and refused to overwrite an existing backup. The real plugin bootstrap is a separate runtime test, not covered by these file-deployment checks. As dev, run nvim --headless ~/.config/nvim/init.lua -c 'luafile /etc/nixos/neovim-test.lua' for a bounded runtime check of Lazy, the configured Mason tools (including executable startup), and nine parsers. This uses the existing writable plugin/Mason cache and may download dependencies; it does not edit the managed Lua or save buffers.
Run the graphical test separately with ./audit-desktop.sh. An optional accessible render node, for example ./audit-desktop.sh /dev/dri/renderD128, moves rendering out of QEMU's CPU emulation without touching the host display. Screenshots and logs go to ~/.cache/desktop-audit/run.*. The test uses an isolated VM, test-only credentials and an emulated sound card with a silent backend. It checks session services, fonts, 100%/150% scaling, real PAM lock/unlock, speaker volume, microphone mute and audio controls. Hardware audio, NVIDIA, suspend and mixed-monitor behavior still need the target workstation. nix flake check without --no-build also runs the software-rendered VM and can be very slow without KVM.
Make focused changes, format/evaluate them, and commit regularly. Build and activate a reviewed commit rather than accumulating uncommitted setup. system.configurationRevision records the source revision in the system generation. Do not put human identity guesses in Git settings or push to an unapproved remote.
Updates and safety
- EC2 only:
nixos-update.timerchecks daily with up to one hour of jitter. Nixpkgs/Home Manager release branches and the latest developer-tool package input advance; Neovim's source stays fixed. The updater skips a dirty repository, builds in a detached worktree, records a tested commit and applies it without rebooting. It attempts rollback if activation fails and retains recovery generations. Inspectjournalctl -u nixos-updateor trigger it withsudo systemctl start nixos-update. - For a manual input refresh:
nix flake update nixpkgs home-manager nixpkgs-latest, reviewflake.lock, then check/build. The exact resolved revisions and hashes remain committed. - Keep the EC2 module on the EC2 target only. Preserve sandboxing, signature verification and each host's recovery access.
system.stateVersionandhome.stateVersionare both26.05; these preserve compatibility, not package versions.testactivates changes too; it is not a dry run. Keep the original system generation.- Rollbacks do not restore mutable user/application data, lockfile updates, backups, or this Git working tree.
- No private keys, plaintext secrets, build outputs or agent notes in this repo.