Share dev environment between dev and nb hosts #2

Open
opened 2026-04-29 20:18:57 +02:00 by dominik.polakovics · 1 comment

Problem Statement

Today the dev host (a microvm on fw) and the nb host (laptop) carry two parallel implementations of "Dominik's dev environment." The CLI tooling overlaps but drifts: nb has bento, drone-cli, gcc, gnumake, go, hugo, postgresql, tea, uv, etc. that dev does not; dev has htop, screen, curl that nb does not. User-level config (programs.git, programs.tmux, project clones) lives only on nb, because dev does not have home-manager — instead it has a systemd-deploy workaround at utils/home-manager/claude-code/nixos.nix that exists solely because home-manager's default mode runs nix-env --set against /nix/store, which is read-only on the microvm.

The result: switching between dev and nb is a friction point. Tools you rely on aren't where you expect them. Adding a tool means remembering to mirror it. Some "dev" things on nb (code-cursor, vscode) are no longer wanted at all.

Solution

Introduce two shared modules: a NixOS-level dev module and a home-manager-level dev module. Both hosts import both. Graphical-only configuration (firefox, thunderbird, chromium, gtk, dconf, signald, clicknload-proxy, cryptomator, mail accounts, ssh matchBlocks with epicenter identity files, mcp-chromium, libvirtd) stays on nb. code-cursor and vscode are removed entirely. Real home-manager runs on dev via home-manager.useUserPackages = true; home-manager.useGlobalPkgs = true;, which routes user packages to /etc/profiles/per-user/dominik (built into the system closure at switch time) and bypasses the read-only-store problem that motivated the systemd-deploy workaround. That workaround module is removed.

User Stories

  1. As the dev-host user, I want the full CLI dev tool union (ddev, docker-compose, git, git-lfs, git-filter-repo, mkcert, php, nodejs_22, gcc, gnumake, glib, bento, drone-cli, hugo, flutter, supabase-cli, air, go, jq, nssTools, mqttui, nix-prefetch-git, postgresql, rbw, sops, tea, unzip, uv, vim, wget, wireguard-tools, wkhtmltopdf, wol, claude-code, screen, htop, curl) on both dev and nb, so that I never wonder which host has what.
  2. As the dev-host user, I want my programs.git configuration (user.name, user.email, lfs, branch.sort, rerere, forgejo URL rewrite) identical on both hosts, so that commits and clones behave the same wherever I work.
  3. As the dev-host user, I want neovim with all LSPs (typescript-language-server, lua-language-server, intelephense, gopls, vscode-langservers-extracted, yaml-language-server), lazygit, ripgrep, aider-chat, and the full plugin set on both hosts, so that I have full IDE features when editing over SSH.
  4. As the dev-host user, I want programs.tmux configured the same on both hosts, so that long-running terminal sessions feel consistent.
  5. As the dev-host user, I want oh-my-zsh (steeef theme + git plugin) plus the cr = "claude --resume" alias and the foot-terminal Shift+Return binding on both hosts, so that shell behavior is consistent.
  6. As the dev-host user, I want all my project repos auto-cloned on both hosts via the same mechanism, so that I can pick up work on any machine.
  7. As the dev-host user, I want claude-code, the codex CLI shim, docker, gcc, go, php, postgresql, etc. on dev, so that I can run any project there.
  8. As the dev-host user, I want programs.adb enabled and the adbusers group present on both hosts, so that the adb binary is on $PATH consistently (USB passthrough not expected on dev).
  9. As the dev-host user, I want flutter available on both hosts, so that I can run mobile-related dev tasks anywhere.
  10. As the nb (laptop) user, I want firefox, thunderbird, chromium, mail accounts, signald, clicknload-proxy, gtk theming, dconf settings, xdg.mimeApps, cryptomator symlinks, and wallpaper file drops to remain unchanged after the refactor.
  11. As the nb user, I want all my SSH matchBlocks (including epicenter, dearmep, akvorrat, whoidentifies, hilgenberg, nycro, wsw, amz) to remain on nb only, so that they don't try to use missing identity files on dev.
  12. As the nb user, I want mcp-chromium to remain nb-only, since its wrapper hardcodes Wayland which dev does not have.
  13. As the nb user, I want virtualisation.libvirtd to remain nb-only, since dev is itself a microvm without a display.
  14. As the nb user, I want my OPENAI_API_KEY shellInit (sops-driven) and the SOPS_AGE_KEY_FILE session variable to keep working unchanged.
  15. As the nb user, I want code-cursor and vscode removed from my system closure, so that I rely on neovim and Claude Code instead.
  16. As the maintainer, I want a single source of truth for "the dev environment," so that adding a CLI tool to one host can't drift from the other.
  17. As the maintainer, I want home-manager to run on dev despite its read-only /nix/store, so that I can use real programs.git and HM activation hooks instead of file-drop systemd workarounds.
  18. As the maintainer, I want utils/home-manager/claude-code/nixos.nix deleted after the refactor, so that there's only one HM mechanism in the repo.
  19. As the maintainer, I want dev's systemd.services.clone-repos removed, so that clone logic isn't duplicated between a system service and HM activation.
  20. As the maintainer, I want the residual hosts/nb/modules/development/coding.nix (whose only remaining content after removing vscode/cursor would be a single ELECTRON_OZONE_PLATFORM_HINT env var) inlined into nb/configuration.nix and the file deleted.
  21. As the maintainer, I want both shared modules importable independently, so that future hosts can adopt one without the other.
  22. As the maintainer, I want the existing utils/home-manager/claude-code import preserved on both hosts, so that Claude skills + statusline keep working on both.
  23. As the maintainer, I want nb's next nixos-rebuild switch to not break working systemd user services or graphical apps, accepting that user packages relocate from ~/.nix-profile to /etc/profiles/per-user/dominik and that logged-in shells need re-login to pick up the new PATH.
  24. As the maintainer, I want ./scripts/test-configuration nb to pass after the refactor, so that I have confidence before pushing.
  25. As a future host onboarder, I want adding a new dev host to require importing two modules (utils/modules/development + utils/home-manager/development) and setting two HM flags (useUserPackages + useGlobalPkgs), so that bootstrapping a new dev environment is one page of config.

Implementation Decisions

Module design.

  • A new shared NixOS module utils/modules/development encapsulates all CLI dev tooling, virtualisation.docker.enable, programs.adb.enable, nixpkgs.config.android_sdk.accept_license = true, users.users.dominik.extraGroups = [ "docker" "adbusers" ] (additive merge with each host's own list), and the full programs.zsh block (enable, ohMyZsh steeef + git plugin, the foot Shift+Return binding, the cr alias, users.defaultUserShell = pkgs.zsh).
  • The neovim configuration moves verbatim from hosts/nb/modules/development/nvim/ to utils/modules/development/nvim/ (including the chatgpt.nix overlay file). The shared NixOS dev module imports it.
  • A new shared HM module utils/home-manager/development encapsulates programs.git, programs.bash, programs.tmux (moved from NixOS-level on dev), home.enableNixpkgsReleaseCheck = false, and the full home.activation.projects clone list. It imports a sub-file codex-cli.nix (moved from hosts/nb/users/codex-cli.nix) for the codex npm-bin install.
  • Module name is development, not dev, to avoid collision with the host name dev in narrative and import paths.

Home-manager mode.

  • Both hosts set home-manager.useUserPackages = true; home-manager.useGlobalPkgs = true; in their respective NixOS module composition. This is the documented "advanced configuration" pattern for HM-as-NixOS-module: it routes user packages to /etc/profiles/per-user/<username> (built into the system closure at switch time) instead of ~/.nix-profile (which requires nix-env --set against /nix/store and fails on the microvm). It also reuses the system's pkgs instance, which makes the nixpkgs.config.allowUnfree = true line currently inside nb's HM block redundant — that line is removed.
  • dev therefore gains real home-manager for the first time. hosts/dev/users/dominik.nix switches from importing claude-code/nixos.nix to defining home-manager.users.dominik = { imports = [ ../utils/home-manager/claude-code ../utils/home-manager/development ]; home.stateVersion = "22.05"; }.

Scope inclusions / exclusions.

  • Included on both hosts: every CLI tool currently on either host's dev module, plus flutter and the android stack (programs.adb, adbusers group, android_sdk.accept_license).
  • Excluded from dev, kept on nb: virtualisation.libvirtd (nested virt cost on a microvm running on Hetzner — Hetzner Cloud commonly does not expose nested KVM, and even if it did, no display means virt-manager is moot, only virsh would be usable), mcp-chromium (its wrapper hardcodes Wayland flags which dev cannot provide).
  • Removed from both hosts: code-cursor, vscode.
  • Not shared at HM level: all programs.ssh matchBlocks (many reference ~/.ssh/epicenter.id_rsa / ~/.ssh/epicenter_id_ed25519 which are not present on dev's persisted home), the OPENAI_API_KEY shellInit + SOPS_AGE_KEY_FILE sessionVariable (sops not yet wired on dev), MOZ_ENABLE_WAYLAND sessionVariable (graphical), and all graphical HM bits (firefox, thunderbird, chromium, gtk, dconf, signald, clicknload-proxy, accounts.email, xdg.mimeApps, cryptomator symlinks, wallpapers, nvim project_history file drop, cryptomator JSON drop).

Project clone list.

  • Moved verbatim from hosts/nb/users/dominik.nix home.activation.projects into the shared HM module. Both hosts now run the full ~50-repo list. dev's existing systemd.services.clone-repos (and its cloneScript let-binding and repositories list) is removed from hosts/dev/configuration.nix.
  • Some clones in the list use SSH remotes that resolve only with epicenter keys, which are not on dev. These will fail at activation time on dev; the activation script's set +eu makes those failures non-fatal and silent (2>/dev/null).

Existing module adaptations.

  • hosts/dev/modules/dev-tools.nix is deleted; its content moves into the shared NixOS dev module.
  • hosts/nb/modules/development/default.nix slims to virtualisation.libvirtd plus an import of mcp-chromium.nix. All packages, the nvim import, and the coding.nix import are dropped.
  • hosts/nb/modules/development/coding.nix is deleted; its sole remaining content (a single ELECTRON_OZONE_PLATFORM_HINT = "auto" session variable) is inlined into hosts/nb/configuration.nix next to MOZ_ENABLE_WAYLAND.
  • hosts/nb/modules/development/mcp-chromium.nix is unchanged (stays nb-local).
  • utils/home-manager/claude-code/nixos.nix is deleted; the systemd-deploy workaround is no longer needed.
  • hosts/nb/users/dominik.nix drops programs.git and the redundant nixpkgs.config.allowUnfree, and adds ../utils/home-manager/development to the HM imports list (next to the existing ../utils/home-manager/claude-code).
  • hosts/nb/users/codex-cli.nix is moved to utils/home-manager/development/codex-cli.nix.

Composition contract.

  • A future host adopting "dev environment" imports ./utils/modules/development in configuration.nix, sets home-manager.useUserPackages = true; home-manager.useGlobalPkgs = true;, and inside its home-manager.users.<user> block adds ../utils/home-manager/development to the imports list. That is the entire onboarding cost.

Testing Decisions

A good test here verifies the external behavior of the module composition: does the host's NixOS closure evaluate and dry-build successfully? Internal implementation (the exact attrset shape of the modules, which file contains which package list, the relative ordering of imports) is not tested — those are implementation details that can change without breaking the host.

  • ./scripts/test-configuration nb must pass after the refactor. This is the gate before push, already established by CLAUDE.md.
  • ./scripts/test-configuration dev is not part of the test plan, because that script does not work for the dev host (its configuration is only consumed indirectly via fw's microvm.vms.dev block, not as a standalone NixOS system).
  • Prior art: the test-configuration dry-build is the standing pre-push gate for every host change in this repo.

Out of Scope

  • Adding sops to dev (and provisioning an openai_api_key secret there). Without it, the shared HM module cannot ship the OPENAI_API_KEY shellInit, so it stays nb-local. Future work, separate PRD.
  • Sharing SSH matchBlocks to dev. Would require deploying epicenter ssh keys (epicenter.id_rsa, epicenter_id_ed25519) into dev's persisted home (/var/lib/microvm-persist/dev/home/dominik/.ssh/). Future work.
  • Replacing the (builtins.fetchTarball ...) home-manager pinning with a flake-based input.
  • Replacing the unstable = import (fetchTarball ...) pattern in nb/users/dominik.nix with an overlay-based approach.
  • Touching utils/home-manager/claude-code (the active default.nix, settings, statusline, skills) — only the obsolete nixos.nix workaround file is removed.
  • Migrating any deployments (fueltide, ai-mailer, etc.) onto dev. Only the clone list changes; actual project deployment is unchanged.
  • Adding a unit-test framework for nix modules. The dry-build is the only testing mechanism in scope.

Further Notes

  • After the next nixos-rebuild switch on nb, user packages relocate from ~/.nix-profile to /etc/profiles/per-user/dominik (because useUserPackages = true flips on). Logged-in shells may have stale PATH until re-login; running services pinned via systemd will pick up new paths on restart automatically.
  • dev will pull flutter and the android-related closure (~several GB) into its rootfs (which is sized 51200MB). Fits, but it's a meaningful chunk — flagged in case it exceeds expectations.
  • libvirtd and mcp-chromium were each evaluated for inclusion on dev before being ruled out. The reasoning is recorded above so future revisits don't have to redo the discovery.
  • The shared HM module's clone list contains repos using both forgejo@git.cloonar.com: and git@github.com: — both work with the default ssh key. Only the git@gitlab.epicenter.works: and *.dearmep.eu/*.whoidentifies.me paths require the nb-only epicenter keys; clones using those will silently fail on dev per the activation script's set +eu and 2>/dev/null pattern.
## Problem Statement Today the `dev` host (a microvm on `fw`) and the `nb` host (laptop) carry two parallel implementations of "Dominik's dev environment." The CLI tooling overlaps but drifts: `nb` has `bento`, `drone-cli`, `gcc`, `gnumake`, `go`, `hugo`, `postgresql`, `tea`, `uv`, etc. that `dev` does not; `dev` has `htop`, `screen`, `curl` that `nb` does not. User-level config (`programs.git`, `programs.tmux`, project clones) lives only on `nb`, because `dev` does not have home-manager — instead it has a systemd-deploy workaround at `utils/home-manager/claude-code/nixos.nix` that exists solely because home-manager's default mode runs `nix-env --set` against `/nix/store`, which is read-only on the microvm. The result: switching between `dev` and `nb` is a friction point. Tools you rely on aren't where you expect them. Adding a tool means remembering to mirror it. Some "dev" things on `nb` (`code-cursor`, `vscode`) are no longer wanted at all. ## Solution Introduce two shared modules: a NixOS-level dev module and a home-manager-level dev module. Both hosts import both. Graphical-only configuration (firefox, thunderbird, chromium, gtk, dconf, signald, clicknload-proxy, cryptomator, mail accounts, ssh matchBlocks with epicenter identity files, mcp-chromium, libvirtd) stays on `nb`. `code-cursor` and `vscode` are removed entirely. Real home-manager runs on `dev` via `home-manager.useUserPackages = true; home-manager.useGlobalPkgs = true;`, which routes user packages to `/etc/profiles/per-user/dominik` (built into the system closure at switch time) and bypasses the read-only-store problem that motivated the systemd-deploy workaround. That workaround module is removed. ## User Stories 1. As the dev-host user, I want the full CLI dev tool union (`ddev`, `docker-compose`, `git`, `git-lfs`, `git-filter-repo`, `mkcert`, `php`, `nodejs_22`, `gcc`, `gnumake`, `glib`, `bento`, `drone-cli`, `hugo`, `flutter`, `supabase-cli`, `air`, `go`, `jq`, `nssTools`, `mqttui`, `nix-prefetch-git`, `postgresql`, `rbw`, `sops`, `tea`, `unzip`, `uv`, `vim`, `wget`, `wireguard-tools`, `wkhtmltopdf`, `wol`, `claude-code`, `screen`, `htop`, `curl`) on both `dev` and `nb`, so that I never wonder which host has what. 2. As the dev-host user, I want my `programs.git` configuration (user.name, user.email, lfs, branch.sort, rerere, forgejo URL rewrite) identical on both hosts, so that commits and clones behave the same wherever I work. 3. As the dev-host user, I want neovim with all LSPs (`typescript-language-server`, `lua-language-server`, `intelephense`, `gopls`, `vscode-langservers-extracted`, `yaml-language-server`), `lazygit`, `ripgrep`, `aider-chat`, and the full plugin set on both hosts, so that I have full IDE features when editing over SSH. 4. As the dev-host user, I want `programs.tmux` configured the same on both hosts, so that long-running terminal sessions feel consistent. 5. As the dev-host user, I want oh-my-zsh (steeef theme + git plugin) plus the `cr = "claude --resume"` alias and the foot-terminal Shift+Return binding on both hosts, so that shell behavior is consistent. 6. As the dev-host user, I want all my project repos auto-cloned on both hosts via the same mechanism, so that I can pick up work on any machine. 7. As the dev-host user, I want `claude-code`, the `codex` CLI shim, `docker`, `gcc`, `go`, `php`, `postgresql`, etc. on `dev`, so that I can run any project there. 8. As the dev-host user, I want `programs.adb` enabled and the `adbusers` group present on both hosts, so that the `adb` binary is on `$PATH` consistently (USB passthrough not expected on `dev`). 9. As the dev-host user, I want `flutter` available on both hosts, so that I can run mobile-related dev tasks anywhere. 10. As the nb (laptop) user, I want firefox, thunderbird, chromium, mail accounts, signald, clicknload-proxy, gtk theming, dconf settings, xdg.mimeApps, cryptomator symlinks, and wallpaper file drops to remain unchanged after the refactor. 11. As the nb user, I want all my SSH `matchBlocks` (including epicenter, dearmep, akvorrat, whoidentifies, hilgenberg, nycro, wsw, amz) to remain on `nb` only, so that they don't try to use missing identity files on `dev`. 12. As the nb user, I want `mcp-chromium` to remain `nb`-only, since its wrapper hardcodes Wayland which `dev` does not have. 13. As the nb user, I want `virtualisation.libvirtd` to remain `nb`-only, since `dev` is itself a microvm without a display. 14. As the nb user, I want my `OPENAI_API_KEY` shellInit (sops-driven) and the `SOPS_AGE_KEY_FILE` session variable to keep working unchanged. 15. As the nb user, I want `code-cursor` and `vscode` removed from my system closure, so that I rely on neovim and Claude Code instead. 16. As the maintainer, I want a single source of truth for "the dev environment," so that adding a CLI tool to one host can't drift from the other. 17. As the maintainer, I want home-manager to run on `dev` despite its read-only `/nix/store`, so that I can use real `programs.git` and HM activation hooks instead of file-drop systemd workarounds. 18. As the maintainer, I want `utils/home-manager/claude-code/nixos.nix` deleted after the refactor, so that there's only one HM mechanism in the repo. 19. As the maintainer, I want `dev`'s `systemd.services.clone-repos` removed, so that clone logic isn't duplicated between a system service and HM activation. 20. As the maintainer, I want the residual `hosts/nb/modules/development/coding.nix` (whose only remaining content after removing vscode/cursor would be a single `ELECTRON_OZONE_PLATFORM_HINT` env var) inlined into `nb/configuration.nix` and the file deleted. 21. As the maintainer, I want both shared modules importable independently, so that future hosts can adopt one without the other. 22. As the maintainer, I want the existing `utils/home-manager/claude-code` import preserved on both hosts, so that Claude skills + statusline keep working on both. 23. As the maintainer, I want `nb`'s next `nixos-rebuild switch` to not break working systemd user services or graphical apps, accepting that user packages relocate from `~/.nix-profile` to `/etc/profiles/per-user/dominik` and that logged-in shells need re-login to pick up the new PATH. 24. As the maintainer, I want `./scripts/test-configuration nb` to pass after the refactor, so that I have confidence before pushing. 25. As a future host onboarder, I want adding a new dev host to require importing two modules (`utils/modules/development` + `utils/home-manager/development`) and setting two HM flags (`useUserPackages` + `useGlobalPkgs`), so that bootstrapping a new dev environment is one page of config. ## Implementation Decisions **Module design.** - A new shared NixOS module `utils/modules/development` encapsulates all CLI dev tooling, `virtualisation.docker.enable`, `programs.adb.enable`, `nixpkgs.config.android_sdk.accept_license = true`, `users.users.dominik.extraGroups = [ "docker" "adbusers" ]` (additive merge with each host's own list), and the full `programs.zsh` block (enable, ohMyZsh steeef + git plugin, the foot Shift+Return binding, the `cr` alias, `users.defaultUserShell = pkgs.zsh`). - The neovim configuration moves verbatim from `hosts/nb/modules/development/nvim/` to `utils/modules/development/nvim/` (including the `chatgpt.nix` overlay file). The shared NixOS dev module imports it. - A new shared HM module `utils/home-manager/development` encapsulates `programs.git`, `programs.bash`, `programs.tmux` (moved from NixOS-level on `dev`), `home.enableNixpkgsReleaseCheck = false`, and the full `home.activation.projects` clone list. It imports a sub-file `codex-cli.nix` (moved from `hosts/nb/users/codex-cli.nix`) for the codex npm-bin install. - Module name is `development`, not `dev`, to avoid collision with the host name `dev` in narrative and import paths. **Home-manager mode.** - Both hosts set `home-manager.useUserPackages = true; home-manager.useGlobalPkgs = true;` in their respective NixOS module composition. This is the documented "advanced configuration" pattern for HM-as-NixOS-module: it routes user packages to `/etc/profiles/per-user/<username>` (built into the system closure at switch time) instead of `~/.nix-profile` (which requires `nix-env --set` against `/nix/store` and fails on the microvm). It also reuses the system's `pkgs` instance, which makes the `nixpkgs.config.allowUnfree = true` line currently inside `nb`'s HM block redundant — that line is removed. - `dev` therefore gains real home-manager for the first time. `hosts/dev/users/dominik.nix` switches from importing `claude-code/nixos.nix` to defining `home-manager.users.dominik = { imports = [ ../utils/home-manager/claude-code ../utils/home-manager/development ]; home.stateVersion = "22.05"; }`. **Scope inclusions / exclusions.** - Included on both hosts: every CLI tool currently on either host's dev module, plus `flutter` and the android stack (`programs.adb`, `adbusers` group, `android_sdk.accept_license`). - Excluded from `dev`, kept on `nb`: `virtualisation.libvirtd` (nested virt cost on a microvm running on Hetzner — Hetzner Cloud commonly does not expose nested KVM, and even if it did, no display means `virt-manager` is moot, only `virsh` would be usable), `mcp-chromium` (its wrapper hardcodes Wayland flags which `dev` cannot provide). - Removed from both hosts: `code-cursor`, `vscode`. - Not shared at HM level: all `programs.ssh` matchBlocks (many reference `~/.ssh/epicenter.id_rsa` / `~/.ssh/epicenter_id_ed25519` which are not present on `dev`'s persisted home), the `OPENAI_API_KEY` shellInit + `SOPS_AGE_KEY_FILE` sessionVariable (sops not yet wired on `dev`), `MOZ_ENABLE_WAYLAND` sessionVariable (graphical), and all graphical HM bits (firefox, thunderbird, chromium, gtk, dconf, signald, clicknload-proxy, accounts.email, xdg.mimeApps, cryptomator symlinks, wallpapers, nvim project_history file drop, cryptomator JSON drop). **Project clone list.** - Moved verbatim from `hosts/nb/users/dominik.nix` `home.activation.projects` into the shared HM module. Both hosts now run the full ~50-repo list. `dev`'s existing `systemd.services.clone-repos` (and its `cloneScript` let-binding and `repositories` list) is removed from `hosts/dev/configuration.nix`. - Some clones in the list use SSH remotes that resolve only with epicenter keys, which are not on `dev`. These will fail at activation time on `dev`; the activation script's `set +eu` makes those failures non-fatal and silent (`2>/dev/null`). **Existing module adaptations.** - `hosts/dev/modules/dev-tools.nix` is deleted; its content moves into the shared NixOS dev module. - `hosts/nb/modules/development/default.nix` slims to `virtualisation.libvirtd` plus an import of `mcp-chromium.nix`. All packages, the nvim import, and the coding.nix import are dropped. - `hosts/nb/modules/development/coding.nix` is deleted; its sole remaining content (a single `ELECTRON_OZONE_PLATFORM_HINT = "auto"` session variable) is inlined into `hosts/nb/configuration.nix` next to `MOZ_ENABLE_WAYLAND`. - `hosts/nb/modules/development/mcp-chromium.nix` is unchanged (stays nb-local). - `utils/home-manager/claude-code/nixos.nix` is deleted; the systemd-deploy workaround is no longer needed. - `hosts/nb/users/dominik.nix` drops `programs.git` and the redundant `nixpkgs.config.allowUnfree`, and adds `../utils/home-manager/development` to the HM imports list (next to the existing `../utils/home-manager/claude-code`). - `hosts/nb/users/codex-cli.nix` is moved to `utils/home-manager/development/codex-cli.nix`. **Composition contract.** - A future host adopting "dev environment" imports `./utils/modules/development` in `configuration.nix`, sets `home-manager.useUserPackages = true; home-manager.useGlobalPkgs = true;`, and inside its `home-manager.users.<user>` block adds `../utils/home-manager/development` to the imports list. That is the entire onboarding cost. ## Testing Decisions A good test here verifies the external behavior of the module composition: does the host's NixOS closure evaluate and dry-build successfully? Internal implementation (the exact attrset shape of the modules, which file contains which package list, the relative ordering of imports) is not tested — those are implementation details that can change without breaking the host. - **`./scripts/test-configuration nb`** must pass after the refactor. This is the gate before push, already established by `CLAUDE.md`. - `./scripts/test-configuration dev` is **not** part of the test plan, because that script does not work for the `dev` host (its configuration is only consumed indirectly via `fw`'s `microvm.vms.dev` block, not as a standalone NixOS system). - Prior art: the `test-configuration` dry-build is the standing pre-push gate for every host change in this repo. ## Out of Scope - Adding sops to `dev` (and provisioning an `openai_api_key` secret there). Without it, the shared HM module cannot ship the `OPENAI_API_KEY` shellInit, so it stays nb-local. Future work, separate PRD. - Sharing SSH `matchBlocks` to `dev`. Would require deploying epicenter ssh keys (`epicenter.id_rsa`, `epicenter_id_ed25519`) into `dev`'s persisted home (`/var/lib/microvm-persist/dev/home/dominik/.ssh/`). Future work. - Replacing the `(builtins.fetchTarball ...)` home-manager pinning with a flake-based input. - Replacing the `unstable = import (fetchTarball ...)` pattern in `nb/users/dominik.nix` with an overlay-based approach. - Touching `utils/home-manager/claude-code` (the active `default.nix`, settings, statusline, skills) — only the obsolete `nixos.nix` workaround file is removed. - Migrating any deployments (fueltide, ai-mailer, etc.) onto `dev`. Only the clone list changes; actual project deployment is unchanged. - Adding a unit-test framework for nix modules. The dry-build is the only testing mechanism in scope. ## Further Notes - After the next `nixos-rebuild switch` on `nb`, user packages relocate from `~/.nix-profile` to `/etc/profiles/per-user/dominik` (because `useUserPackages = true` flips on). Logged-in shells may have stale `PATH` until re-login; running services pinned via systemd will pick up new paths on restart automatically. - `dev` will pull `flutter` and the android-related closure (~several GB) into its rootfs (which is sized 51200MB). Fits, but it's a meaningful chunk — flagged in case it exceeds expectations. - `libvirtd` and `mcp-chromium` were each evaluated for inclusion on `dev` before being ruled out. The reasoning is recorded above so future revisits don't have to redo the discovery. - The shared HM module's clone list contains repos using both `forgejo@git.cloonar.com:` and `git@github.com:` — both work with the default ssh key. Only the `git@gitlab.epicenter.works:` and `*.dearmep.eu`/`*.whoidentifies.me` paths require the nb-only epicenter keys; clones using those will silently fail on `dev` per the activation script's `set +eu` and `2>/dev/null` pattern.
dominik.polakovics added the
enhancement
ready-for-agent
labels 2026-04-29 20:18:57 +02:00
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Consolidate the dev-environment definition for dev and nb into shared NixOS + home-manager modules, switch dev to real home-manager, and drop the systemd-deploy workaround.

Current behavior:
The CLI tooling, neovim/zsh/tmux/git config, and project clone list for "Dominik's dev environment" are duplicated and drifting between two hosts: dev (a microvm) and nb (laptop). dev does not run home-manager — it uses a custom systemd-deploy module to materialise Claude Code config because home-manager's default nix-env --set mode fails against dev's read-only /nix/store. Project clones on dev are handled by a separate systemd.services.clone-repos instead of home-manager activation. code-cursor and vscode are still installed on nb but are no longer wanted.

Desired behavior:
A single NixOS module + a single home-manager module own the dev environment. Both hosts import both. Graphical-only / nb-specific bits stay on nb. dev gains real home-manager via useUserPackages = true; useGlobalPkgs = true;, which routes user packages to /etc/profiles/per-user/<user> and bypasses the read-only-store problem. The systemd-deploy workaround module is deleted. code-cursor and vscode are removed from the closure entirely.

The detailed PRD — Problem Statement, 25 user stories, Implementation Decisions (module names, HM mode, scope inclusions/exclusions, project clone list, existing module adaptations, composition contract), Testing Decisions, Out of Scope, Further Notes — is in the issue body above and is the authoritative spec. This brief adds two caveats and the acceptance gate.

Caveats the agent must respect:

  1. dev's persisted home is load-bearing. dev runs as a microvm with persistence at /var/lib/microvm-persist/dev/home/dominik/. The HM activation must not wipe or overwrite this directory's contents. Treat existing files (especially .ssh/, project clones already on disk, .config/ state) as preserved across the switch.
  2. Preserve the silent-failure pattern in the project-clone activation. The current home.activation.projects script wraps work in set +eu and redirects stderr with 2>/dev/null so that clones using SSH remotes that don't resolve on a given host (e.g. git@gitlab.epicenter.works:, *.dearmep.eu, *.whoidentifies.me — these need nb-only epicenter keys) fail silently rather than aborting activation. Preserve those exact flags verbatim when moving the activation block into the shared HM module. Do not "improve" the error handling.

Key contracts:

  • A new shared NixOS module owns: the full CLI tool union from both hosts plus flutter and the android stack; virtualisation.docker.enable; programs.adb.enable; nixpkgs.config.android_sdk.accept_license = true; the users.users.dominik.extraGroups additions for docker and adbusers (additive merge); the full programs.zsh block including oh-my-zsh steeef theme + git plugin, the foot-terminal Shift+Return binding, and the cr = "claude --resume" alias; users.defaultUserShell = pkgs.zsh; the neovim configuration moved verbatim (including the chatgpt overlay file).
  • A new shared home-manager module owns: programs.git (user.name, user.email, lfs, branch.sort, rerere, forgejo URL rewrite); programs.bash; programs.tmux; home.enableNixpkgsReleaseCheck = false; the full home.activation.projects clone list from nb; an import of the codex-cli npm-bin sub-module moved from nb's users dir.
  • Both hosts set home-manager.useUserPackages = true; home-manager.useGlobalPkgs = true;.
  • Stays nb-only at NixOS level: virtualisation.libvirtd, the mcp-chromium wrapper.
  • Stays nb-only at HM level: all programs.ssh matchBlocks, the OPENAI_API_KEY shellInit + SOPS_AGE_KEY_FILE sessionVariable, MOZ_ENABLE_WAYLAND, all graphical HM bits (firefox, thunderbird, chromium, gtk, dconf, signald, clicknload-proxy, accounts.email, xdg.mimeApps, cryptomator symlinks, wallpapers, the nvim project_history file drop, the cryptomator JSON drop).
  • Removed from both hosts entirely: code-cursor, vscode. The residual coding.nix on nb (which after removal contains only an ELECTRON_OZONE_PLATFORM_HINT = "auto" session variable) is inlined into nb's top-level configuration and the file deleted.
  • The systemd-deploy claude-code workaround module on dev is deleted; the active utils/home-manager/claude-code module remains imported on both hosts. dev's systemd.services.clone-repos (and its cloneScript let-binding and repositories list) is removed in favour of the HM activation.
  • nb's redundant nixpkgs.config.allowUnfree = true inside its HM block is dropped, since useGlobalPkgs = true reuses the system pkgs instance.

Composition contract for future hosts: import the shared NixOS dev module in configuration.nix, set the two HM flags above, and add the shared HM dev module to the user's HM imports. That should be the entire onboarding cost.

Acceptance criteria:

  • ./scripts/test-configuration nb passes after the refactor (this is the standing pre-push gate per CLAUDE.md).
  • The shared NixOS dev module and the shared HM dev module exist under utils/ and are imported by both dev and nb.
  • dev's closure no longer references the systemd-deploy claude-code workaround module — that file is deleted.
  • dev's closure no longer contains systemd.services.clone-repos — clones are driven by HM activation.
  • nb's closure does not contain code-cursor or vscode.
  • dev's closure contains the full CLI tool union listed in user story 1, plus flutter and the android stack.
  • Both hosts produce identical programs.git configuration (user.name, user.email, lfs, branch.sort, rerere, forgejo URL rewrite).
  • Both hosts produce identical neovim, tmux, and zsh configurations (including the cr alias and the foot Shift+Return binding).
  • Both hosts run the full project-clone list from the moved HM activation, with set +eu and 2>/dev/null preserved verbatim.
  • programs.adb is enabled and dominik is in the adbusers group on both hosts.
  • nb's SSH matchBlocks, OPENAI_API_KEY shellInit, SOPS_AGE_KEY_FILE sessionVariable, MOZ_ENABLE_WAYLAND, libvirtd, mcp-chromium, and all graphical HM bits remain on nb only.
  • nb's coding.nix is deleted; ELECTRON_OZONE_PLATFORM_HINT = "auto" lives in nb's top-level configuration next to MOZ_ENABLE_WAYLAND.

Out of scope (do not address in this issue):

  • Adding sops to dev and provisioning an openai_api_key secret there. Without sops on dev, the shared HM module cannot ship the OPENAI_API_KEY shellInit — it stays nb-only. Future work, separate issue.
  • Sharing SSH matchBlocks to dev. Would require deploying epicenter keys (epicenter.id_rsa, epicenter_id_ed25519) into dev's persisted home. Future work.
  • Replacing the (builtins.fetchTarball ...) home-manager pin with a flake input.
  • Replacing the unstable = import (fetchTarball ...) pattern in nb's user file with an overlay.
  • Touching the active utils/home-manager/claude-code module (default.nix, settings, statusline, skills) — only the obsolete nixos.nix workaround file is removed.
  • Migrating any deployments (fueltide, ai-mailer, etc.) onto dev. Only the clone list changes; actual project deployment is unchanged.
  • Adding a unit-test framework for nix modules. The dry-build is the only testing mechanism in scope.
  • ./scripts/test-configuration dev — that script does not work for dev (its config is consumed indirectly via fw's microvm.vms.dev block, not as a standalone NixOS system).

Notes for the agent:

  • After the next nixos-rebuild switch on nb, user packages relocate from ~/.nix-profile to /etc/profiles/per-user/dominik because useUserPackages = true flips on. Logged-in shells may have stale PATH until re-login. Flag this in the PR description.
  • dev will pull flutter + the android closure (~several GB) into its rootfs (sized 51200MB). It fits, but flag it in the PR description for visibility.
  • Module name is development (not dev) to avoid collision with the host name dev in import paths and narrative.
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Consolidate the dev-environment definition for `dev` and `nb` into shared NixOS + home-manager modules, switch `dev` to real home-manager, and drop the systemd-deploy workaround. **Current behavior:** The CLI tooling, neovim/zsh/tmux/git config, and project clone list for "Dominik's dev environment" are duplicated and drifting between two hosts: `dev` (a microvm) and `nb` (laptop). `dev` does not run home-manager — it uses a custom systemd-deploy module to materialise Claude Code config because home-manager's default `nix-env --set` mode fails against `dev`'s read-only `/nix/store`. Project clones on `dev` are handled by a separate `systemd.services.clone-repos` instead of home-manager activation. `code-cursor` and `vscode` are still installed on `nb` but are no longer wanted. **Desired behavior:** A single NixOS module + a single home-manager module own the dev environment. Both hosts import both. Graphical-only / nb-specific bits stay on `nb`. `dev` gains real home-manager via `useUserPackages = true; useGlobalPkgs = true;`, which routes user packages to `/etc/profiles/per-user/<user>` and bypasses the read-only-store problem. The systemd-deploy workaround module is deleted. `code-cursor` and `vscode` are removed from the closure entirely. The detailed PRD — Problem Statement, 25 user stories, Implementation Decisions (module names, HM mode, scope inclusions/exclusions, project clone list, existing module adaptations, composition contract), Testing Decisions, Out of Scope, Further Notes — is in the issue body above and is the authoritative spec. This brief adds two caveats and the acceptance gate. **Caveats the agent must respect:** 1. **`dev`'s persisted home is load-bearing.** `dev` runs as a microvm with persistence at `/var/lib/microvm-persist/dev/home/dominik/`. The HM activation must not wipe or overwrite this directory's contents. Treat existing files (especially `.ssh/`, project clones already on disk, `.config/` state) as preserved across the switch. 2. **Preserve the silent-failure pattern in the project-clone activation.** The current `home.activation.projects` script wraps work in `set +eu` and redirects stderr with `2>/dev/null` so that clones using SSH remotes that don't resolve on a given host (e.g. `git@gitlab.epicenter.works:`, `*.dearmep.eu`, `*.whoidentifies.me` — these need nb-only epicenter keys) fail silently rather than aborting activation. Preserve those exact flags verbatim when moving the activation block into the shared HM module. Do not "improve" the error handling. **Key contracts:** - A new shared NixOS module owns: the full CLI tool union from both hosts plus `flutter` and the android stack; `virtualisation.docker.enable`; `programs.adb.enable`; `nixpkgs.config.android_sdk.accept_license = true`; the `users.users.dominik.extraGroups` additions for `docker` and `adbusers` (additive merge); the full `programs.zsh` block including oh-my-zsh `steeef` theme + `git` plugin, the foot-terminal Shift+Return binding, and the `cr = "claude --resume"` alias; `users.defaultUserShell = pkgs.zsh`; the neovim configuration moved verbatim (including the chatgpt overlay file). - A new shared home-manager module owns: `programs.git` (user.name, user.email, lfs, branch.sort, rerere, forgejo URL rewrite); `programs.bash`; `programs.tmux`; `home.enableNixpkgsReleaseCheck = false`; the full `home.activation.projects` clone list from `nb`; an import of the codex-cli npm-bin sub-module moved from `nb`'s users dir. - Both hosts set `home-manager.useUserPackages = true; home-manager.useGlobalPkgs = true;`. - Stays nb-only at NixOS level: `virtualisation.libvirtd`, the mcp-chromium wrapper. - Stays nb-only at HM level: all `programs.ssh` matchBlocks, the `OPENAI_API_KEY` shellInit + `SOPS_AGE_KEY_FILE` sessionVariable, `MOZ_ENABLE_WAYLAND`, all graphical HM bits (firefox, thunderbird, chromium, gtk, dconf, signald, clicknload-proxy, accounts.email, xdg.mimeApps, cryptomator symlinks, wallpapers, the nvim project_history file drop, the cryptomator JSON drop). - Removed from both hosts entirely: `code-cursor`, `vscode`. The residual `coding.nix` on `nb` (which after removal contains only an `ELECTRON_OZONE_PLATFORM_HINT = "auto"` session variable) is inlined into `nb`'s top-level configuration and the file deleted. - The systemd-deploy claude-code workaround module on `dev` is deleted; the active `utils/home-manager/claude-code` module remains imported on both hosts. `dev`'s `systemd.services.clone-repos` (and its `cloneScript` let-binding and `repositories` list) is removed in favour of the HM activation. - `nb`'s redundant `nixpkgs.config.allowUnfree = true` inside its HM block is dropped, since `useGlobalPkgs = true` reuses the system pkgs instance. **Composition contract for future hosts:** import the shared NixOS dev module in `configuration.nix`, set the two HM flags above, and add the shared HM dev module to the user's HM imports. That should be the entire onboarding cost. **Acceptance criteria:** - [ ] `./scripts/test-configuration nb` passes after the refactor (this is the standing pre-push gate per CLAUDE.md). - [ ] The shared NixOS dev module and the shared HM dev module exist under `utils/` and are imported by both `dev` and `nb`. - [ ] `dev`'s closure no longer references the systemd-deploy claude-code workaround module — that file is deleted. - [ ] `dev`'s closure no longer contains `systemd.services.clone-repos` — clones are driven by HM activation. - [ ] `nb`'s closure does not contain `code-cursor` or `vscode`. - [ ] `dev`'s closure contains the full CLI tool union listed in user story 1, plus `flutter` and the android stack. - [ ] Both hosts produce identical `programs.git` configuration (user.name, user.email, lfs, branch.sort, rerere, forgejo URL rewrite). - [ ] Both hosts produce identical neovim, tmux, and zsh configurations (including the `cr` alias and the foot Shift+Return binding). - [ ] Both hosts run the full project-clone list from the moved HM activation, with `set +eu` and `2>/dev/null` preserved verbatim. - [ ] `programs.adb` is enabled and `dominik` is in the `adbusers` group on both hosts. - [ ] `nb`'s SSH matchBlocks, `OPENAI_API_KEY` shellInit, `SOPS_AGE_KEY_FILE` sessionVariable, `MOZ_ENABLE_WAYLAND`, libvirtd, mcp-chromium, and all graphical HM bits remain on `nb` only. - [ ] `nb`'s `coding.nix` is deleted; `ELECTRON_OZONE_PLATFORM_HINT = "auto"` lives in `nb`'s top-level configuration next to `MOZ_ENABLE_WAYLAND`. **Out of scope (do not address in this issue):** - Adding sops to `dev` and provisioning an `openai_api_key` secret there. Without sops on `dev`, the shared HM module cannot ship the `OPENAI_API_KEY` shellInit — it stays nb-only. Future work, separate issue. - Sharing SSH matchBlocks to `dev`. Would require deploying epicenter keys (`epicenter.id_rsa`, `epicenter_id_ed25519`) into `dev`'s persisted home. Future work. - Replacing the `(builtins.fetchTarball ...)` home-manager pin with a flake input. - Replacing the `unstable = import (fetchTarball ...)` pattern in `nb`'s user file with an overlay. - Touching the active `utils/home-manager/claude-code` module (default.nix, settings, statusline, skills) — only the obsolete `nixos.nix` workaround file is removed. - Migrating any deployments (fueltide, ai-mailer, etc.) onto `dev`. Only the clone list changes; actual project deployment is unchanged. - Adding a unit-test framework for nix modules. The dry-build is the only testing mechanism in scope. - `./scripts/test-configuration dev` — that script does not work for `dev` (its config is consumed indirectly via `fw`'s `microvm.vms.dev` block, not as a standalone NixOS system). **Notes for the agent:** - After the next `nixos-rebuild switch` on `nb`, user packages relocate from `~/.nix-profile` to `/etc/profiles/per-user/dominik` because `useUserPackages = true` flips on. Logged-in shells may have stale `PATH` until re-login. Flag this in the PR description. - `dev` will pull `flutter` + the android closure (~several GB) into its rootfs (sized 51200MB). It fits, but flag it in the PR description for visibility. - Module name is `development` (not `dev`) to avoid collision with the host name `dev` in import paths and narrative.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cloonar/nixos#2
No description provided.