Share dev environment between dev and nb hosts #2
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cloonar/nixos#2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem Statement
Today the
devhost (a microvm onfw) and thenbhost (laptop) carry two parallel implementations of "Dominik's dev environment." The CLI tooling overlaps but drifts:nbhasbento,drone-cli,gcc,gnumake,go,hugo,postgresql,tea,uv, etc. thatdevdoes not;devhashtop,screen,curlthatnbdoes not. User-level config (programs.git,programs.tmux, project clones) lives only onnb, becausedevdoes not have home-manager — instead it has a systemd-deploy workaround atutils/home-manager/claude-code/nixos.nixthat exists solely because home-manager's default mode runsnix-env --setagainst/nix/store, which is read-only on the microvm.The result: switching between
devandnbis a friction point. Tools you rely on aren't where you expect them. Adding a tool means remembering to mirror it. Some "dev" things onnb(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-cursorandvscodeare removed entirely. Real home-manager runs ondevviahome-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
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 bothdevandnb, so that I never wonder which host has what.programs.gitconfiguration (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.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.programs.tmuxconfigured the same on both hosts, so that long-running terminal sessions feel consistent.cr = "claude --resume"alias and the foot-terminal Shift+Return binding on both hosts, so that shell behavior is consistent.claude-code, thecodexCLI shim,docker,gcc,go,php,postgresql, etc. ondev, so that I can run any project there.programs.adbenabled and theadbusersgroup present on both hosts, so that theadbbinary is on$PATHconsistently (USB passthrough not expected ondev).flutteravailable on both hosts, so that I can run mobile-related dev tasks anywhere.matchBlocks(including epicenter, dearmep, akvorrat, whoidentifies, hilgenberg, nycro, wsw, amz) to remain onnbonly, so that they don't try to use missing identity files ondev.mcp-chromiumto remainnb-only, since its wrapper hardcodes Wayland whichdevdoes not have.virtualisation.libvirtdto remainnb-only, sincedevis itself a microvm without a display.OPENAI_API_KEYshellInit (sops-driven) and theSOPS_AGE_KEY_FILEsession variable to keep working unchanged.code-cursorandvscoderemoved from my system closure, so that I rely on neovim and Claude Code instead.devdespite its read-only/nix/store, so that I can use realprograms.gitand HM activation hooks instead of file-drop systemd workarounds.utils/home-manager/claude-code/nixos.nixdeleted after the refactor, so that there's only one HM mechanism in the repo.dev'ssystemd.services.clone-reposremoved, so that clone logic isn't duplicated between a system service and HM activation.hosts/nb/modules/development/coding.nix(whose only remaining content after removing vscode/cursor would be a singleELECTRON_OZONE_PLATFORM_HINTenv var) inlined intonb/configuration.nixand the file deleted.utils/home-manager/claude-codeimport preserved on both hosts, so that Claude skills + statusline keep working on both.nb's nextnixos-rebuild switchto not break working systemd user services or graphical apps, accepting that user packages relocate from~/.nix-profileto/etc/profiles/per-user/dominikand that logged-in shells need re-login to pick up the new PATH../scripts/test-configuration nbto pass after the refactor, so that I have confidence before pushing.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.
utils/modules/developmentencapsulates 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 fullprograms.zshblock (enable, ohMyZsh steeef + git plugin, the foot Shift+Return binding, thecralias,users.defaultUserShell = pkgs.zsh).hosts/nb/modules/development/nvim/toutils/modules/development/nvim/(including thechatgpt.nixoverlay file). The shared NixOS dev module imports it.utils/home-manager/developmentencapsulatesprograms.git,programs.bash,programs.tmux(moved from NixOS-level ondev),home.enableNixpkgsReleaseCheck = false, and the fullhome.activation.projectsclone list. It imports a sub-filecodex-cli.nix(moved fromhosts/nb/users/codex-cli.nix) for the codex npm-bin install.development, notdev, to avoid collision with the host namedevin narrative and import paths.Home-manager mode.
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 requiresnix-env --setagainst/nix/storeand fails on the microvm). It also reuses the system'spkgsinstance, which makes thenixpkgs.config.allowUnfree = trueline currently insidenb's HM block redundant — that line is removed.devtherefore gains real home-manager for the first time.hosts/dev/users/dominik.nixswitches from importingclaude-code/nixos.nixto defininghome-manager.users.dominik = { imports = [ ../utils/home-manager/claude-code ../utils/home-manager/development ]; home.stateVersion = "22.05"; }.Scope inclusions / exclusions.
flutterand the android stack (programs.adb,adbusersgroup,android_sdk.accept_license).dev, kept onnb: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 meansvirt-manageris moot, onlyvirshwould be usable),mcp-chromium(its wrapper hardcodes Wayland flags whichdevcannot provide).code-cursor,vscode.programs.sshmatchBlocks (many reference~/.ssh/epicenter.id_rsa/~/.ssh/epicenter_id_ed25519which are not present ondev's persisted home), theOPENAI_API_KEYshellInit +SOPS_AGE_KEY_FILEsessionVariable (sops not yet wired ondev),MOZ_ENABLE_WAYLANDsessionVariable (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.
hosts/nb/users/dominik.nixhome.activation.projectsinto the shared HM module. Both hosts now run the full ~50-repo list.dev's existingsystemd.services.clone-repos(and itscloneScriptlet-binding andrepositorieslist) is removed fromhosts/dev/configuration.nix.dev. These will fail at activation time ondev; the activation script'sset +eumakes those failures non-fatal and silent (2>/dev/null).Existing module adaptations.
hosts/dev/modules/dev-tools.nixis deleted; its content moves into the shared NixOS dev module.hosts/nb/modules/development/default.nixslims tovirtualisation.libvirtdplus an import ofmcp-chromium.nix. All packages, the nvim import, and the coding.nix import are dropped.hosts/nb/modules/development/coding.nixis deleted; its sole remaining content (a singleELECTRON_OZONE_PLATFORM_HINT = "auto"session variable) is inlined intohosts/nb/configuration.nixnext toMOZ_ENABLE_WAYLAND.hosts/nb/modules/development/mcp-chromium.nixis unchanged (stays nb-local).utils/home-manager/claude-code/nixos.nixis deleted; the systemd-deploy workaround is no longer needed.hosts/nb/users/dominik.nixdropsprograms.gitand the redundantnixpkgs.config.allowUnfree, and adds../utils/home-manager/developmentto the HM imports list (next to the existing../utils/home-manager/claude-code).hosts/nb/users/codex-cli.nixis moved toutils/home-manager/development/codex-cli.nix.Composition contract.
./utils/modules/developmentinconfiguration.nix, setshome-manager.useUserPackages = true; home-manager.useGlobalPkgs = true;, and inside itshome-manager.users.<user>block adds../utils/home-manager/developmentto 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 nbmust pass after the refactor. This is the gate before push, already established byCLAUDE.md../scripts/test-configuration devis not part of the test plan, because that script does not work for thedevhost (its configuration is only consumed indirectly viafw'smicrovm.vms.devblock, not as a standalone NixOS system).test-configurationdry-build is the standing pre-push gate for every host change in this repo.Out of Scope
dev(and provisioning anopenai_api_keysecret there). Without it, the shared HM module cannot ship theOPENAI_API_KEYshellInit, so it stays nb-local. Future work, separate PRD.matchBlockstodev. Would require deploying epicenter ssh keys (epicenter.id_rsa,epicenter_id_ed25519) intodev's persisted home (/var/lib/microvm-persist/dev/home/dominik/.ssh/). Future work.(builtins.fetchTarball ...)home-manager pinning with a flake-based input.unstable = import (fetchTarball ...)pattern innb/users/dominik.nixwith an overlay-based approach.utils/home-manager/claude-code(the activedefault.nix, settings, statusline, skills) — only the obsoletenixos.nixworkaround file is removed.dev. Only the clone list changes; actual project deployment is unchanged.Further Notes
nixos-rebuild switchonnb, user packages relocate from~/.nix-profileto/etc/profiles/per-user/dominik(becauseuseUserPackages = trueflips on). Logged-in shells may have stalePATHuntil re-login; running services pinned via systemd will pick up new paths on restart automatically.devwill pullflutterand 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.libvirtdandmcp-chromiumwere each evaluated for inclusion ondevbefore being ruled out. The reasoning is recorded above so future revisits don't have to redo the discovery.forgejo@git.cloonar.com:andgit@github.com:— both work with the default ssh key. Only thegit@gitlab.epicenter.works:and*.dearmep.eu/*.whoidentifies.mepaths require the nb-only epicenter keys; clones using those will silently fail ondevper the activation script'sset +euand2>/dev/nullpattern.Agent Brief
Category: enhancement
Summary: Consolidate the dev-environment definition for
devandnbinto shared NixOS + home-manager modules, switchdevto 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) andnb(laptop).devdoes not run home-manager — it uses a custom systemd-deploy module to materialise Claude Code config because home-manager's defaultnix-env --setmode fails againstdev's read-only/nix/store. Project clones ondevare handled by a separatesystemd.services.clone-reposinstead of home-manager activation.code-cursorandvscodeare still installed onnbbut 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.devgains real home-manager viauseUserPackages = 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-cursorandvscodeare 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:
dev's persisted home is load-bearing.devruns 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.home.activation.projectsscript wraps work inset +euand redirects stderr with2>/dev/nullso 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:
flutterand the android stack;virtualisation.docker.enable;programs.adb.enable;nixpkgs.config.android_sdk.accept_license = true; theusers.users.dominik.extraGroupsadditions fordockerandadbusers(additive merge); the fullprograms.zshblock including oh-my-zshsteeeftheme +gitplugin, the foot-terminal Shift+Return binding, and thecr = "claude --resume"alias;users.defaultUserShell = pkgs.zsh; the neovim configuration moved verbatim (including the chatgpt overlay file).programs.git(user.name, user.email, lfs, branch.sort, rerere, forgejo URL rewrite);programs.bash;programs.tmux;home.enableNixpkgsReleaseCheck = false; the fullhome.activation.projectsclone list fromnb; an import of the codex-cli npm-bin sub-module moved fromnb's users dir.home-manager.useUserPackages = true; home-manager.useGlobalPkgs = true;.virtualisation.libvirtd, the mcp-chromium wrapper.programs.sshmatchBlocks, theOPENAI_API_KEYshellInit +SOPS_AGE_KEY_FILEsessionVariable,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).code-cursor,vscode. The residualcoding.nixonnb(which after removal contains only anELECTRON_OZONE_PLATFORM_HINT = "auto"session variable) is inlined intonb's top-level configuration and the file deleted.devis deleted; the activeutils/home-manager/claude-codemodule remains imported on both hosts.dev'ssystemd.services.clone-repos(and itscloneScriptlet-binding andrepositorieslist) is removed in favour of the HM activation.nb's redundantnixpkgs.config.allowUnfree = trueinside its HM block is dropped, sinceuseGlobalPkgs = truereuses 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 nbpasses after the refactor (this is the standing pre-push gate per CLAUDE.md).utils/and are imported by bothdevandnb.dev's closure no longer references the systemd-deploy claude-code workaround module — that file is deleted.dev's closure no longer containssystemd.services.clone-repos— clones are driven by HM activation.nb's closure does not containcode-cursororvscode.dev's closure contains the full CLI tool union listed in user story 1, plusflutterand the android stack.programs.gitconfiguration (user.name, user.email, lfs, branch.sort, rerere, forgejo URL rewrite).cralias and the foot Shift+Return binding).set +euand2>/dev/nullpreserved verbatim.programs.adbis enabled anddominikis in theadbusersgroup on both hosts.nb's SSH matchBlocks,OPENAI_API_KEYshellInit,SOPS_AGE_KEY_FILEsessionVariable,MOZ_ENABLE_WAYLAND, libvirtd, mcp-chromium, and all graphical HM bits remain onnbonly.nb'scoding.nixis deleted;ELECTRON_OZONE_PLATFORM_HINT = "auto"lives innb's top-level configuration next toMOZ_ENABLE_WAYLAND.Out of scope (do not address in this issue):
devand provisioning anopenai_api_keysecret there. Without sops ondev, the shared HM module cannot ship theOPENAI_API_KEYshellInit — it stays nb-only. Future work, separate issue.dev. Would require deploying epicenter keys (epicenter.id_rsa,epicenter_id_ed25519) intodev's persisted home. Future work.(builtins.fetchTarball ...)home-manager pin with a flake input.unstable = import (fetchTarball ...)pattern innb's user file with an overlay.utils/home-manager/claude-codemodule (default.nix, settings, statusline, skills) — only the obsoletenixos.nixworkaround file is removed.dev. Only the clone list changes; actual project deployment is unchanged../scripts/test-configuration dev— that script does not work fordev(its config is consumed indirectly viafw'smicrovm.vms.devblock, not as a standalone NixOS system).Notes for the agent:
nixos-rebuild switchonnb, user packages relocate from~/.nix-profileto/etc/profiles/per-user/dominikbecauseuseUserPackages = trueflips on. Logged-in shells may have stalePATHuntil re-login. Flag this in the PR description.devwill pullflutter+ the android closure (~several GB) into its rootfs (sized 51200MB). It fits, but flag it in the PR description for visibility.development(notdev) to avoid collision with the host namedevin import paths and narrative.