diff --git a/.gitignore b/.gitignore index 2089a65..6990134 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ /result-* /.direnv/ +# NixOS-generated files when the checkout lives at /etc/nix. +/nix.conf +/registry.json + # Local secrets must never be imported into Nix or committed. /.env /.env.* diff --git a/README.md b/README.md index 896181a..d2b75f8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@ -# Development host +# 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 base, platform, Nix features, compatibility version, imports | -| `users.nix` | `dev`, SSH/sudo, Home Manager integration, workspace/repo ownership | +| `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 | @@ -14,23 +19,23 @@ Flat, explicit NixOS modules, with locked inputs. Required machine setup belongs | `workstation.nix`, `nvidia.nix` | Separately selected physical-workstation/greeter and NVIDIA integration; not enabled on EC2 | | `updates.nix`, `update-system.sh` | Daily stable-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` | systemd-resolved and network/WireGuard diagnostics; leaves interface management with the host | +| `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](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`; existing SSM-user administration and root SSH recovery remain intact. +- Passwordless sudo is scoped to `dev`; EC2's SSM-user administration and root SSH recovery remain intact. The physical host retains `kbot`'s existing wheel/sudo access. - `dev` is authorized by the public key in this repo. No private key or password is embedded. -- `/etc/nixos` is writable by `dev` through a native tmpfiles ownership rule, which does not follow store symlinks. +- On EC2 only, `/etc/nixos` is writable by `dev` through a native tmpfiles ownership rule, which does not follow store symlinks. The physical target leaves the original `/etc/nixos` recovery configs and `/home/kbot` untouched; `/etc/nix` remains 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-Backspace` resets it. `y` opens Yazi with shell-directory integration. - Desktop keys: `Super-Enter` Kitty, `Super-Space` launcher, `Super-E` Yazi, `Super-B` Firefox, `Super-P` KeePassXC, `Super-Escape` lock, `Super-Q` close, `Super-1…0` workspaces, `Print` screenshot/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 physical-workstation module requires a securely provisioned `hashedPasswordFile`; EC2 gets no greeter or autologin. +- 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 provisioned `hashedPasswordFile`; the physical host keeps SDDM and a locally set `dev` password. 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` and `wg-quick` are installed without any tunnels, peers, keys or added firewall ports. `resolvectl` is backed by resolved; DHCP remains under the existing host network manager. +- `wg`, `wg-quick`, OpenVPN, `iperf3`, `nmap`, `traceroute`, `whois`, `mtr`, DNS tools, `tcpdump`, `ethtool`, `nc` and `socat` are 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`. @@ -38,7 +43,68 @@ Git author identity and remote destination are deliberately unset. `user.useConf ## Build and apply -As `dev`, from `/etc/nixos`: +### 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: + +```sh +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: + +```sh +cd /etc/nix +nix --extra-experimental-features 'nix-command flakes' flake check --no-build --no-update-lock-file +nix --extra-experimental-features 'nix-command flakes' 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 --extra-experimental-features 'nix-command flakes' 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: + +```sh +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: + +```sh +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: + +```sh +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: ```sh nixfmt --check ./*.nix @@ -93,9 +159,9 @@ Make focused changes, format/evaluate them, and commit regularly. Build and acti ## Updates and safety -- `nixos-update.timer` checks daily with up to one hour of jitter. Only the stable Nixpkgs/Home Manager branches 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. Inspect `journalctl -u nixos-update` or trigger it with `sudo systemctl start nixos-update`. +- **EC2 only:** `nixos-update.timer` checks daily with up to one hour of jitter. Only the stable Nixpkgs/Home Manager branches 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. Inspect `journalctl -u nixos-update` or trigger it with `sudo systemctl start nixos-update`. - For a manual input refresh: `nix flake update nixpkgs home-manager`, review `flake.lock`, then check/build. The exact resolved revisions and hashes remain committed. -- Keep the EC2 module, sandboxing, signature verification and recovery access intact. +- Keep the EC2 module on the EC2 target only. Preserve sandboxing, signature verification and each host's recovery access. - `system.stateVersion` and `home.stateVersion` are both `26.05`; these preserve compatibility, not package versions. - `test` activates 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. diff --git a/common.nix b/common.nix new file mode 100644 index 0000000..833917c --- /dev/null +++ b/common.nix @@ -0,0 +1,29 @@ +# Shared dev environment; boot, storage, recovery users and updates are host-specific. +{ config, inputs, ... }: +{ + imports = [ + ./users.nix + ./tools.nix + ./network.nix + ./desktop.nix + ./apps.nix + ./neovim.nix + ]; + + nixpkgs.hostPlatform = "x86_64-linux"; + + nix = { + channel.enable = false; + # Keep non-login environments on the same pin when NIX_PATH is unset. + settings.nix-path = config.nix.nixPath; + settings.experimental-features = [ + "nix-command" + "flakes" + ]; + }; + + system.configurationRevision = inputs.self.rev or inputs.self.dirtyRev or null; + + # Initial data/default compatibility, not the desired package release. + system.stateVersion = "26.05"; +} diff --git a/configuration.nix b/configuration.nix index fbea355..f2bfacc 100644 --- a/configuration.nix +++ b/configuration.nix @@ -1,37 +1,13 @@ -{ - config, - inputs, - modulesPath, - ... -}: - +# EC2 only. Physical machines must use the separate nixos host target. +{ modulesPath, ... }: { imports = [ # Keep the image's EC2 boot, storage, metadata, SSH and SSM integration. "${modulesPath}/virtualisation/amazon-image.nix" - ./users.nix - ./tools.nix - ./network.nix - ./desktop.nix - ./apps.nix + ./common.nix ./updates.nix - ./neovim.nix ]; - nixpkgs.hostPlatform = "x86_64-linux"; - - nix = { - channel.enable = false; - # Keep non-login environments on the same pin when NIX_PATH is unset. - settings.nix-path = config.nix.nixPath; - settings.experimental-features = [ - "nix-command" - "flakes" - ]; - }; - - system.configurationRevision = inputs.self.rev or inputs.self.dirtyRev or null; - - # Initial data/default compatibility, not the desired package release. - system.stateVersion = "26.05"; + # Only the EC2 host keeps its dev-owned configuration checkout here. + systemd.tmpfiles.rules = [ "Z /etc/nixos - dev users -" ]; } diff --git a/flake.nix b/flake.nix index 1e36383..661d7e1 100644 --- a/flake.nix +++ b/flake.nix @@ -27,6 +27,15 @@ ]; }; + # Physical host: adds dev/Hyprland without removing kbot/Plasma. + nixosConfigurations.nixos = nixpkgs.lib.nixosSystem { + specialArgs = { inherit inputs; }; + modules = [ + ./physical.nix + home-manager.nixosModules.home-manager + ]; + }; + formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt; nixosModules = { @@ -43,6 +52,10 @@ in { desktop = import ./desktop-test.nix { inherit inputs pkgs; }; + physical-config = import ./physical-test.nix { + inherit pkgs; + config = inputs.self.nixosConfigurations.nixos.config; + }; desktop-config = pkgs.runCommand "hyprland-config-check" { nativeBuildInputs = [ pkgs.hyprland ]; } '' diff --git a/hardware-configuration.nix b/hardware-configuration.nix new file mode 100644 index 0000000..2aea370 --- /dev/null +++ b/hardware-configuration.nix @@ -0,0 +1,60 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ + config, + lib, + pkgs, + modulesPath, + ... +}: + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ + "xhci_pci" + "thunderbolt" + "nvme" + "usb_storage" + "sd_mod" + "sdhci_pci" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-uuid/8a16015f-d6f8-4f74-8558-6261b9112216"; + fsType = "btrfs"; + }; + + fileSystems."/home" = { + device = "/dev/disk/by-uuid/8a16015f-d6f8-4f74-8558-6261b9112216"; + fsType = "btrfs"; + options = [ "subvol=home" ]; + }; + + fileSystems."/nix" = { + device = "/dev/disk/by-uuid/8a16015f-d6f8-4f74-8558-6261b9112216"; + fsType = "btrfs"; + options = [ "subvol=nix" ]; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-uuid/DEC5-51CB"; + fsType = "vfat"; + options = [ + "fmask=0077" + "dmask=0077" + ]; + }; + + swapDevices = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.intel.npu.enable = true; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/network.nix b/network.nix index 675c1ae..69a1941 100644 --- a/network.nix +++ b/network.nix @@ -1,10 +1,11 @@ -{ pkgs, ... }: +{ lib, pkgs, ... }: { # Keep each host's existing interface/DHCP owner. On EC2 this is dhcpcd; # NixOS wires its resolvconf hook to resolved's compatibility interface. services.resolved = { - enable = true; + # Physical hosts can retain their existing DNS setup during migration. + enable = lib.mkDefault true; settings.Resolve = { LLMNR = false; MulticastDNS = false; @@ -14,12 +15,17 @@ programs.mtr.enable = true; environment.systemPackages = with pkgs; [ - wireguard-tools + wireguard-tools # wg and wg-quick; no interfaces or credentials are configured. + openvpn + iperf3 + nmap + traceroute + whois dnsutils tcpdump ethtool netcat-openbsd socat ]; - # wg/wg-quick are available, but no invented peers, keys, routes or ports. + # Client tools only: no VPN services, peers, keys, routes or firewall ports. } diff --git a/physical-test.nix b/physical-test.nix new file mode 100644 index 0000000..a5ee393 --- /dev/null +++ b/physical-test.nix @@ -0,0 +1,141 @@ +# Host configuration and built-file checks: no activation, VM or VPN connections. +{ config, pkgs }: +let + inherit (pkgs) lib; + kbot = config.users.users.kbot; + dev = config.users.users.dev; + btrfsDevice = "/dev/disk/by-uuid/8a16015f-d6f8-4f74-8558-6261b9112216"; + mounts = { + "/" = { + device = btrfsDevice; + fsType = "btrfs"; + }; + "/home" = { + device = btrfsDevice; + fsType = "btrfs"; + options = [ "subvol=home" ]; + }; + "/nix" = { + device = btrfsDevice; + fsType = "btrfs"; + options = [ "subvol=nix" ]; + }; + "/boot" = { + device = "/dev/disk/by-uuid/DEC5-51CB"; + fsType = "vfat"; + options = [ + "fmask=0077" + "dmask=0077" + ]; + }; + }; + tests = [ + { + assertion = config.boot.loader.systemd-boot.enable && !config.boot.loader.grub.enable; + message = "The physical host must use systemd-boot, never EC2's GRUB disk."; + } + { + assertion = lib.all ( + mount: + let + actual = config.fileSystems.${mount}; + expected = mounts.${mount}; + in + actual.device == expected.device + && actual.fsType == expected.fsType + && lib.all (option: builtins.elem option actual.options) (expected.options or [ ]) + ) (builtins.attrNames mounts); + message = "Preserve the installed root/home/nix/EFI filesystems and Btrfs subvolumes."; + } + { + assertion = + config.users.mutableUsers + && kbot.isNormalUser + && kbot.uid == 1000 + && kbot.home == "/home/kbot" + && builtins.elem "wheel" kbot.extraGroups + && builtins.elem "networkmanager" kbot.extraGroups + && kbot.password == null + && kbot.hashedPassword == null + && kbot.hashedPasswordFile == null; + message = "Keep kbot's identity, local password and administrative/network access."; + } + { + assertion = + dev.isNormalUser + && dev.uid == 1001 + && dev.home == "/home/dev" + && builtins.elem "wheel" dev.extraGroups + && builtins.elem "networkmanager" dev.extraGroups + && builtins.attrNames config.home-manager.users == [ "dev" ]; + message = "Add dev independently; Home Manager must not manage kbot's home."; + } + { + assertion = + config.services.displayManager.sddm.enable + && config.services.desktopManager.plasma6.enable + && config.programs.hyprland.enable + && config.programs.hyprland.withUWSM + && !config.services.greetd.enable + && !config.services.displayManager.autoLogin.enable; + message = "Keep the Plasma/SDDM recovery login alongside Hyprland, without autologin."; + } + { + assertion = + config.networking.networkmanager.enable + && !config.services.resolved.enable + && builtins.elem (lib.getName pkgs.networkmanager-openvpn) ( + map lib.getName config.networking.networkmanager.plugins + ); + message = "Keep NetworkManager/DNS and provide its OpenVPN integration."; + } + { + assertion = + !(config.systemd.services ? nixos-update) + && !(config.systemd.timers ? nixos-update) + && !(config.systemd.services ? amazon-ssm-agent) + && !(builtins.elem "Z /etc/nixos - dev users -" config.systemd.tmpfiles.rules); + message = "The physical host must not inherit EC2 services, updater or repo ownership."; + } + { + assertion = + lib.all + (package: builtins.elem (lib.getName package) (map lib.getName config.environment.systemPackages)) + ( + with pkgs; + [ + wireguard-tools + openvpn + iperf3 + nmap + traceroute + whois + dnsutils + tcpdump + ethtool + netcat-openbsd + socat + ] + ) + && config.programs.mtr.enable; + message = "The VPN clients and network diagnostics must remain installed."; + } + { + assertion = + config.networking.wireguard.interfaces == { } + && config.networking.wg-quick.interfaces == { } + && config.services.openvpn.servers == { }; + message = "Installing VPN tools must not invent tunnels, peers or credentials."; + } + ]; +in +assert lib.all (test: lib.assertMsg test.assertion test.message) tests; +pkgs.runCommand "physical-config-check" { } '' + sessions=${config.services.displayManager.sessionData.desktops}/share + test -f "$sessions/wayland-sessions/plasma.desktop" + test -f "$sessions/wayland-sessions/hyprland-uwsm.desktop" + for tool in wg wg-quick openvpn iperf3 nmap traceroute whois mtr dig tcpdump ethtool nc socat nm-connection-editor; do + test -x "${config.system.path}/bin/$tool" + done + touch "$out" +'' diff --git a/physical.nix b/physical.nix new file mode 100644 index 0000000..24eaff0 --- /dev/null +++ b/physical.nix @@ -0,0 +1,74 @@ +# This machine: retain the installed kbot/Plasma recovery environment and add dev. +# Hardware settings were copied from /etc/nixos; those original files stay untouched. +{ pkgs, ... }: +{ + imports = [ + ./hardware-configuration.nix + ./common.nix + ]; + + boot.loader = { + systemd-boot.enable = true; + efi.canTouchEfiVariables = true; + }; + + networking.hostName = "nixos"; + networking.networkmanager = { + enable = true; + plugins = [ pkgs.networkmanager-openvpn ]; + }; + # Keep the installed DNS setup while testing the additional desktop. + services.resolved.enable = false; + + time.timeZone = "America/Chicago"; + i18n.defaultLocale = "en_US.UTF-8"; + i18n.extraLocaleSettings = { + LC_ADDRESS = "en_US.UTF-8"; + LC_IDENTIFICATION = "en_US.UTF-8"; + LC_MEASUREMENT = "en_US.UTF-8"; + LC_MONETARY = "en_US.UTF-8"; + LC_NAME = "en_US.UTF-8"; + LC_NUMERIC = "en_US.UTF-8"; + LC_PAPER = "en_US.UTF-8"; + LC_TELEPHONE = "en_US.UTF-8"; + LC_TIME = "en_US.UTF-8"; + }; + + # SDDM offers both Plasma and Hyprland (uwsm-managed). Do not replace it with + # workstation.nix's greetd/ReGreet or enable autologin during this migration. + services.xserver = { + enable = true; + xkb = { + layout = "us"; + variant = ""; + }; + }; + services.displayManager.sddm.enable = true; + services.desktopManager.plasma6.enable = true; + services.printing.enable = true; + services.pulseaudio.enable = false; + + # Preserve kbot's existing password and allow `sudo passwd dev` after activation. + # No password hashes or credentials are copied into this repository/the store. + users.mutableUsers = true; + users.users.kbot = { + isNormalUser = true; + uid = 1000; + home = "/home/kbot"; + description = "kbot"; + extraGroups = [ + "networkmanager" + "wheel" + ]; + packages = [ pkgs.kdePackages.kate ]; + }; + users.users.dev.extraGroups = [ "networkmanager" ]; + environment.systemPackages = [ pkgs.networkmanagerapplet ]; + home-manager.users.dev.services.udiskie.enable = true; + + # Retain the installed host's package policy. + nixpkgs.config.allowUnfree = true; + + # Intentionally no updates.nix: that updater builds the EC2 dev target. + # Neither /etc/nixos nor /home/kbot is handed over to dev/Home Manager. +} diff --git a/users.nix b/users.nix index 5257427..b372cf9 100644 --- a/users.nix +++ b/users.nix @@ -32,7 +32,5 @@ systemd.tmpfiles.rules = [ "d /home/dev/.config 0755 dev users -" "d /home/dev/projects 0755 dev users -" - # Keep the working repo editable by dev. Z does not follow store symlinks. - "Z /etc/nixos - dev users -" ]; }