Objective
Evaluate adopting Gabriel Volpe’s “Private Nix flake” pattern to add a host axis (personal vs. enterprise) on top of the current architecture-only split in Nix - Home Manager, without refactoring this repo or cluttering it with inline override hooks.
Context
Current dotfiles at ~/.config only vary by system (aarch64-darwin, x86_64-darwin, aarch64-linux, x86_64-linux) via forAllSystems in nix/flake.nix. Host-level customization (personal vs. work machine on the same arch) is not supported. Two ad-hoc override hooks exist today:
Read as of 2026-04-24. Both statements have since been overtaken:
local.*capability flags pluslocal.profilecover the role axis (2026-07-04/05), andhosts/local.nixcovers per-host identity (2026-07-19). The “not supported” clause was also never quite right — the corp machines were already customizing downstream through these very hooks.
nix/home-manager/modules/packages.nix:6-12— inlineoverrideAdd/overrideRemovearrays relying ongit rerereto absorb rebase conflicts.nix/home-manager/modules/coding-agents/claude/claude.nix:46-60— sidecarclaude-overrides.nixloaded viabuiltins.pathExists.
Research conducted 2026-04-24 found:
- Home-manager has no first-class host abstraction (HM FAQ just says “one top-level file per user-machine combo”).
- No native “override file” convention under flakes; legacy
~/.config/nixpkgs/overlays/is inert under pure eval. - Community canon splits into four patterns:
hosts/<hostname>/directories (Misterio77, EmergentMind), option-based feature flags (options.my.*),flake-partsper-host modules (srid, Lite-system), and composable profile stacks.
Approach
Prefer downstream-as-input over a host-axis refactor of this repo:
- Public repo (this one) stays minimal and unchanged.
- A separate private flake takes this as a flake input, layers its own
hosts/work.nix+ overlay exposingmkHomeConfigurationsper Volpe’s writeup. - “Rebase on upstream” collapses to
nix flake update <input>on the private flake. - Secrets stay out of the public repo entirely; use sops-nix or agenix in the private flake.
Alternative (rejected for current scope): refactor this repo to Misterio77/EmergentMind-style host axis with hosts/, specialArgs, options.my.*. Touches every module; high churn; only worthwhile if multiple hosts need to co-exist in one repo.
Next Actions
-
Decide whether the enterprise use case is concrete enough to warrant creating a private flake yet, or whether the existingAnswered 2026-07-19, and the question was mis-framed. The corp machines have been running these dotfiles the whole time, via a rebased downstream branch. The hooks were never a stopgap awaiting a decision; they were the mechanism in production. See the 2026-07-19 correction below.overrideAdd/claude-overrides.nixhooks are sufficient for now. - If proceeding: scaffold a private flake with this repo as input, one host module, and a test overlay that swaps one small surface (e.g.
isTrusted, git identity, or a coding-agent API base URL). - Validate that the override-priority mechanics (
lib.mkForce,disabledModules) actually compose cleanly against this repo’s modules without per-module changes. - Consider whether the existing
claude-overrides.nixsidecar pattern should be generalized or retired once the private-flake approach is in place. - Smaller stepping-stone option: refactor one module (e.g.
git.nixorclaude.nix) to declareoptions.dotfiles.<feature>.*withmkEnableOption/mkPackageOption, gated vialib.mkIf. Validates whether a Misterio77-style feature-flag layer composes cleanly with the rest of the tree before committing to the full private-flake refactor. Lower-risk than scaffolding a downstream consumer; reversible if it doesn’t pay off.
Resources
- Gabriel Volpe — “Private Nix flake” — primary reference pattern.
- Lorenz Bischof — “Manage secrets in NixOS with a private repository” — same pattern, secrets framing.
- Michael Maclean — “NixOS with private flakes” — SSH agent forwarding setup.
- Misterio77/nix-config — canonical
hosts/<hostname>/reference (rejected alternative layout). - EmergentMind/nix-config —
hosts/common/{core,optional}layered imports. - srid/nixos-config — flake-parts approach.
- NixOS Discourse — Hostname-agnostic nix-darwin configuration — endorses named configurations and git-branch-per-machine.
- Jade Lovelace — “Flakes aren’t real” — dissenting view worth reading before committing to flake-heavy layering.
- mightyiam/dendritic and vic/den — recently formalized “every file is a flake-parts module” pattern. Filtered out as overkill for the 2-host scale here. Revisit trigger revised 2026-07-19: guard-test coverage, not host count. The original trigger (~5 machines, or shared community modules becoming attractive) turned out to be the weaker signal. Dendritic’s real advantage here is that it collapses axis 1 entirely: with no curated import list, there is no composition-based mechanism that can mask a dead annotation-based flag — the exact bug found 2026-07-19, where
local.desktopApps.enablegated nothing for two weeks while looking correct, because import omission on athena was already producing the right outcome. The cost of collapsing axis 1 is that a missedenableguard reaches the x86_64-darwin build. That cost is falling: Intel breakage measured at zero errors across the full closure (twice), andtests/capability-flags.nixnow detects missed guards directly. The honest trigger is when that test covers the heavy Intel-breaking modules (litellm/arrow-cpp, open-webui, signoz) rather than justdesktopApps/python.tools— at that point the import boundary is protecting against a failure the suite already catches, and axis 1 is pure cost.
Trigger met the same day (2026-07-19, commit 8ae96b1). Contracts now cover all three heavy modules across four surfaces (home.file targets, activation names, launchd agents, package names), verified on all three systems. Extending them surfaced that
programs.litellm.enabledid not exist at all — the module’sconfigwas unconditional, so only import omission kept the arrow-cpp puller off athena. That is the refactor the 2026-07-04 scoping section above called “the one required module refactor,” still undone 15 days later and invisible to every check.The trigger being met does not settle the question, and I am not treating it as settled. The residual argument for axis 1 is narrower than the original: the contract test asserts artifacts disappear, not that the closure is free of Intel-broken derivations. A module could pass its contract and still pull a broken dependency through a path no contract names. That is a real gap, but it is a much smaller one than “a missed guard is silent.” Deciding whether it justifies a hand-maintained import list is a judgement call worth making deliberately rather than by default. See Dendritic Pattern Borrows AOP Vocabulary But Realizes SPL Composition and Nix Module System Realizes Multi-Dimensional Separation of Concerns.
- Discourse — “Pattern: every file is a flake-parts module” — origin thread for the dendritic pattern.
- Academic Foundations of Nix Configuration Patterns — annotated bibliography assembled 2026-04-27 covering Dolstra’s foundational Nix papers, the AOP/MDSoC lineage, and the SPL/FOP literature that arguably fits the multi-host problem better than AOP does.
Notes
Status is paused because the enterprise host isn’t yet a concrete requirement. Unblocked when: (1) a specific work machine materializes, or (2) the current overrideAdd / claude-overrides.nix hooks start causing rebase pain.
Superseded 2026-07-19. Both unblock conditions were written as future events, and both had already happened. The corp Linux workstations exist, and they consume this repo through a rebased downstream branch rather than as a flake input. The homeConfigurations.aarch64-linux / .x86_64-linux entries in nix/flake.nix are those hosts, not portability fixtures.
That reframes the private-flake question rather than answering it. The pattern was reserved here for two things: secrets, and trusted = false. Neither is solved today. What the correction removes is only the precondition — there is no machine to wait for, and the honest comparison is now downstream-branch-plus-sidecar (in production, working) against downstream-as-input (unbuilt). The branch approach has a real advantage the original note did not anticipate: a file that exists only downstream, like hosts/local.nix, never conflicts on rebase, because upstream never touches the path. That is strictly cheaper than the tracked overrideAdd arrays it sits beside, which re-resolve a conflict every rebase via git rerere.
The live cost is therefore narrower than “we need a private flake”: corp hostnames and any corp-specific secrets must not reach the public repo. The sidecar already covers identity. Secrets are the remaining gap, and secrets alone may not justify a downstream flake when sops-nix or agenix could be pointed at a downstream-only path in the same way.
Consequence to keep in view: any change to the desktop profile ships to a corp box on its next rebase. This repo has more than two consumers, and one of them is not personally owned.
Non-obvious finding from research: disabledModules matches by path, so any upstream tree restructure would silently break a downstream consumer relying on it. That argues for keeping module paths stable in this repo once downstream consumers exist, or for preferring lib.mkForce over disabledModules in the private flake.
Academic-frame addition (2026-04-27): the multi-host variability problem is a textbook fit for Tarr et al.’s Multi-Dimensional Separation of Concerns (ICSE 1999), realized over Dolstra & Hemel’s purely-functional configuration paradigm (HotOS 2007). The Software Product Lines / Feature-Oriented Programming literature (Ferreira et al. 2014; Gaia et al. 2014) maps more cleanly to what the Nix module system enables than the “aspect-oriented” framing the dendritic community uses. This doesn’t change the architectural recommendation — Misterio77-style feature flags remain the lowest-friction path — but provides defensible grounding if the choice ever needs to be written up. See Academic Foundations of Nix Configuration Patterns.
Module-system primitives worth remembering for whichever path is taken: mkEnableOption "<feature>" (boolean toggle shorthand), mkPackageOption pkgs "<name>" {} (typed package selection), mkDefault (priority 1000) / mkForce (priority 50) for layered overrides, extraSpecialArgs (vs _module.args) when imports need to depend on host or profile.
Addendum 2026-07-04 — Capability-flag model + athena as concrete trigger
Revisited while scoping a Home Manager rollout to athena (Intel Mac home server, x86_64-darwin). This surfaces a second variability axis the original note never scoped: not personal-vs-enterprise on the same arch, but role + arch (desktop vs. headless server; aarch64- vs. x86_64-darwin). Status flipped paused → active: unblock trigger (2) — “override hooks start causing pain” — is now superseded by a concrete host need.
Chosen mechanism — capabilities-as-options. Each module declares options.dotfiles.<feature>.enable = mkEnableOption ... and wraps its body in lib.mkIf; a per-host module (hosts/<name>.nix) is just the opt-in list; profiles are named bundles (desktop, server, headless, work). This promotes the note’s own “smaller stepping-stone” feature-flag idea to the primary recommendation for the role/arch axis. The private-flake / downstream-as-input pattern stays reserved for the enterprise host only (secrets + trusted = false, consuming this repo as an input).
Machine → profile mapping (capability matrix image generated 2026-07-04 at ~/Pictures/generated/hm-capability-matrix.png):
- atlas — aarch64-darwin,
desktop: everything (desktop-gui, ai-stack on). - athena — x86_64-darwin,
server: shell / neovim / git / dev-tools / media-server / obsidian / remote-mcp; desktop-gui + ai-stack off. - oracle (OCI) — aarch64-linux NixOS,
headless: shell / git / remote-mcp only (HM-as-NixOS-module, not a standalone homeConfiguration). - enterprise —
workvia private flake: dev core + secrets; personal AI / obsidian / mcp off.
Prerequisite the matrix hides: flake.nix:118 intentionally excludes x86_64-darwin (arrow-cpp marked broken, pulled transitively by the LiteLLM stack). athena needs the arch admitted to systems and the homeConfigurations genAttrs list first; the capability flags are what make that admission safe (heavy modules never enter the graph).
Scoping findings (2026-07-04, the real work):
- Most heavy modules already self-gate on
enable(signoz.nix,open-webui.nix,tailscale-serve.nix,obsidian.nix) and are off unlesshome.nixturns them on → not athena blockers. modules/litellm/default.nixhas NO enable option and NOmkIfgate — unconditionally active on Darwin, and the direct arrow-cpp puller. Addingprograms.litellm.enableand wrapping itsconfiginmkIfis the one required module refactor. Caveat: the options block must stay ungated becausejupyter.nixandsillytavern.nixreadconfig.programs.litellm.port/.defaultClaudeModel; only theconfig(packages, launchd agent, files) getsmkIf.modules/python.nix(uv2nix venvs incl. jupyterlab) andshell.nix:106(ghostty-bin, an aarch64-darwin prebuilt) also need gating / arch-widening for x86_64-darwin.- Unbounded risk = Intel-arch whack-a-mole: the full set of x86_64-darwin-broken transitive deps is only discoverable by running
hm buildon athena; each failure reveals the next path to gate oroverrideRemove(the hook atpackages.nix:6-12already supports per-host removal). Static reading cannot enumerate it. Recommended de-risking order: prototype athena with a curated-import entry point (bypass thehome.nixauto-discovery, list only the safe core), get it building, then convert the survivors todotfiles.<feature>flags.
Implementation status (2026-07-04) — step 1 landed (staged, not yet committed):
-
Extracted
nix/home-manager/base.nix(shared host-agnostic identity/XDG/Nix-daemon settings), imported by bothhome.nix(desktop) and the new curatedhome-server.nix(server profile). -
Admitted
x86_64-darwinto the flakesystems(server profile only); refactoredhomeConfigurationsinto amkHomehelper +homeConfigurations."x86_64-darwin" -> home-server.nix; keptchecksdesktop-only. -
shell.nix: ghostty gated off on x86_64-darwin (no prebuilt, headless);uvLibmade optional. Footgun found: a formal-leveluvLib ? nulldefault does not work — the HM module system eagerly passes every declared function arg, shadowing the default. Fix:base.nixsets_module.args.uvLib = lib.mkDefault null, whichpython.nixoverrides on the desktop. -
Verified on atlas:
activationPackage.drvPathbyte-identical before/after (pure refactor,gjlka2k6…); athenahomeConfigurations.x86_64-darwinevaluates (9zc85ann…) so no broken-marked packages in the curated closure;just checkpasses. Capability matrix image at~/Pictures/generated/hm-capability-matrix.png. See 2026-07-04 Home Manager athena Server Profile. Update 2026-07-05 — athena is LIVE. Home Manager is now declaratively managing athena (generation 1 current). Full end-to-end bring-up done; see 2026-07-04 Home Manager athena Server Profile. -
Headline finding: the entire profile — including every desktop GUI app (Zotero, Obsidian, browsers, tectonic) — builds on
x86_64-darwinwith zero Intel breakage. The feared package whack-a-mole never materialised; 0 errors across the full closure. The only cost is time:cache.nixos.orghas thin x86_64-darwin coverage (the sunset), so much of nixpkgs builds from source. The extra caches (nix-community/garnix) don’t mirror it either, sotrusted-usersdoesn’t rescue build speed. -
Bring-up path: Determinate installer has no x86_64-darwin binary (Intel sunset) → used the upstream
nixos.org/nix/install --daemon; adopted~/.configas the repo in place (gh/secrets gitignored, no clobber);extra-trusted-users = achhinain/etc/nix/nix.conf; first activation vianix run home-manager/master -- switch --flake ~/.config/nix#x86_64-darwin -b backup. -
packages.nixtrimmed (commit 4c6f3cb):local.desktopApps.enable(default true) gates the desktop extras (guiApps + documentTools + per-OS GUI bundles); home-server.nix sets it false → athena 114→87 packages, no Zotero/Obsidian/GUI. First realdotfiles-style capability flag (lives underlocal.*for now). -
Two fresh-bootstrap gotchas found + fixed: (1) non-interactive SSH has no
nixon PATH and flakes disabled → drive with the full/nix/var/nix/profiles/default/bin/nixpath +NIX_CONFIG='extra-experimental-features = nix-command flakes'+ exportedXDG_*. (2)mkZshCompletionGenwrote to$XDG_DATA_HOME(runtime env) underset -u; unbound on a bare-shell bootstrap → aborted activation. Fixed by bakingconfig.xdg.dataHomeat store time (commit 183ed19). Never bit desktop hosts (a running HM session always exports XDG_*). -
Commits (all now on
main, see next section): 2aff736 (server profile), 4c6f3cb (packages split), 183ed19 (xdg fix).
Update 2026-07-05 — merged to main + second capability flag (devTools)
Follow-ups from the bring-up landed. All four commits are now on main (fast-forward, both feature branches deleted).
- Merge:
hm-athena-server-profile(3 commits) fast-forwarded intomainand pushed. - Second capability flag —
local.devTools(commit 4badaf5). Added a dev-toolchain master switch with independentlanguageServers/formatters/runtimessub-toggles, following thereleaseCooldownmaster+sub-toggle idiom already inmachine.nix(each sub-toggle defaults to the master).packages.nixgates its three dev groups on these viaoptionalsin original list position. home-server.nix setslocal.devTools.enable = false→ athena 87→60 packages (drops 11 LSPs + 8 formatters/linters + 8 runtimes/compilers). atlasdrvPathbyte-identical before/after (229c3i27…) so the desktop host does not rebuild — theoptionals true xs == xsno-reorder property confirmed empirically via stash-and-diff. - Design decision — the curated import list stays curated. Deliberately did not migrate home-server.nix’s hand-picked imports to full
home.nixauto-discovery + per-module disable flags. Doing so would require a correctenableguard on every Intel-broken heavy module (litellm/arrow-cpp, open-webui, signoz, jupyter, python…); one missed guard re-breaks the x86_64-darwin build. Codified the two independent axes in home-server.nix’s header comment: (1) which modules load = curated imports, the arch-safety boundary, stays hand-managed; (2) which packages loaded modules install = capability flags (desktopApps,devTools), where all content trimming happens. Capability flags currently live underlocal.*(not the aspirationaldotfiles.*namespace) for consistency with the existinglocal.desktopApps.enable. - Deployed to athena (generation 2):
hm switchover SSH applied the trim (87→60 packages live) + the xdg fix; expired generation 1 and GC’d 5.8 GiB (the reclaimed dev toolchain — rust crates, go, nodejs, LSPs).
Update 2026-07-05 (later) — third capability flag: local.python (uv on the server)
Found that uv was not behind any capability flag — it rode inside python.nix, gated purely by module import (desktop auto-discovery pulls it; the server’s curated list didn’t). Promoted it to a real capability so athena can have uv without the heavy data-science tooling.
local.python(machine.nix, commit e1efaef):enable(master, default true — uv/uvx CLI + managed CPython +uvLib+ the local uv2nix tools) plustools.enable(default = master — the heavyuv tool installset: jupyterlab/ipython/comfy-cli/pyzotero/scalene/memray, several pulling torch/Jupyter into~/.local). Same master+sub-toggle idiom asdevTools/releaseCooldown.- python.nix gating:
programs.uv+installPythonVersions+_module.args.uvLibonpython.enable;installUvToolsonpython.enable && tools.enable.uvLibismkIf python.enable {...}so when disabled it falls back to base.nix’smkDefault nulland shell.nix’shasUvgate skips the local tools correctly — no leaky flag. - home-server.nix imports
./modules/python.nixand setslocal.python.tools.enable = false(enable stays true). athena now has uv 0.11.25, managed CPython 3.10–3.15, and the uv2nix-built claude-ops + gh-review-preview, butuv tool listis empty (no torch/jupyter/comfy). - The uv2nix venvs build cleanly on x86_64-darwin — the Intel whack-a-mole again failed to materialise. Also fixed a latent warning:
generateCheckUpstreamIssuesCompletionshelled out touvunconditionally and warned (exit 127) on the uv-less server; gated it onhasUv(commit 26428ab). With uv now present it runs cleanly anyway. - Verification held: atlas
drvPathbyte-identical at229c3i27…across all ofdevTools+python(all flags default true →mkIf/optionalsno-op). Deployed to athena (generation 3), expired generation 2, GC’d another 437.6 MiB. - All on
main: 4badaf5 (devTools), 26428ab (uv completion gate), e1efaef (local.python). Three capability flags now real:desktopApps,devTools,python. - Named profile bundle landed (commit ecef30e):
local.profile(enum desktop|server, default desktop). machine.nix mapsprofile == "server"→{ desktopApps.enable, devTools.enable, python.tools.enable } = mkDefault false; home-server.nix collapses to the singlelocal.profile = "server". mkDefault (1000) beats the option default (mkOptionDefault 1500) but yields to an explicit host override (100), so a server host can still flip one capability back on. Pure refactor — both drvPaths unchanged (atlas229c3i27…viamkIf false; athenagqa9ngn8…, so the athena redeploy was a confirmed no-op, still generation 3). Adding a second server host (oracle) is now a one-linelocal.profile = "server". This realizes the note’s original “profiles = named bundles” recommendation. - Still open: extend to oracle (headless NixOS, HM-as-NixOS-module — the second
profile = "server"consumer, currently the idling OCI box) andthe enterprise private-flake host (still blocked on a concrete machine + secrets)— stale, see the 2026-07-19 correction: the corp hosts exist and already run this repo.
The capability model is now complete for the personal hosts: three flags (desktopApps, devTools, python) + a local.profile bundle, all landed on main (2aff736 · 4c6f3cb · 183ed19 · 4badaf5 · 26428ab · e1efaef · ecef30e), atlas provably unchanged throughout, athena live on all of it.
Update 2026-07-19 — the model was never tested; desktopApps was inert
Review of the landed model (see 2026-07-19 Capability Flags Were Never Tested). The line above — “the capability model is now complete for the personal hosts” — was premature. Two of the three flags were partly decorative.
local.desktopApps.enablegated only a package list. Read atpackages.nix:189and nowhere else.tridactyl/zotero/sillytaverndeployed unconditionally;aerospace/bettertouchtoolgated onisDarwinalone. Its own description claimed it gated the desktop modules. Now guarded withlib.mkIfin all five.local.python.tools.enabledid not gatejupyter/ipythonconfig. Only the activation-timeuv tool installwas gated, so the config files deployed on a host with none of the tools. Both modules now gate on it (not ondesktopApps— this is data-science tooling, not GUI).- Root cause is structural, not sloppiness.
checksbuilds only the desktop profile, where every flag is true, so a flag gating nothing was indistinguishable from a working one. The flags had no false-state coverage for two weeks.
Fix: nix/tests/capability-flags.nix. Re-evaluates the desktop config with each flag off via extendModules and asserts the artifacts disappear. Eval-only, so it does not drag athena’s x86_64-darwin closure into nix flake check (the documented reason home-server.nix is excluded from checks) — that constraint made the obvious “just add athena to checks” fix wrong.
Test-design footgun worth keeping. The first version keyed on home.file attribute names and passed while asserting nothing: xdg.configFile entries land under an absolute key while home.file entries stay relative, so the tridactyl contract matched nothing. Same absolute-key drift as the 2026-07-01 HM bump on settings.json. .target is stable across both; the attribute name is not. The test now also fails on any contract target absent from the baseline, so a vacuous contract cannot pass.
Also landed: assertions for machineName == "localhost" (a silent-wrong-value default that mislabels OTel resource attrs rather than failing) and python.tools.enable -> python.enable (a half-state that installs nothing quietly). Documented the whole model in docs/decisions/0021-capability-flags-host-profiles.md — the first in-repo record; the rationale previously lived only in this note and inline comments.
Two findings recorded but not acted on:
local.python.enable = falseis unsupported on the desktop module set.claude.nixdereferences theuvLibmodule arg unconditionally, whereshell.nixcorrectly guards onhasUv != null. The option description (“set false on profiles that ship no Python at all”) only holds for the curated server import list.- The axis-1 justification is weaker than it reads. The curated import list is justified by Intel breakage, but the bring-up found zero Intel errors across the full closure. arrow-cpp was real, so the decision stands, but the note above already recorded the whack-a-mole failing to materialise twice, and the header comment does not reflect that.
atlas drvPath unchanged (zvdqhbqmm3k9…), verified via a worktree at HEAD rather than stash-and-diff. just check + nix flake check clean.
Next Actions (2026-07-19)
- Decide: guard
uvLibinclaude.nix, or narrow thelocal.python.enabledescription to match reality. -
Done 2026-07-19 (commit fb61d08).home.nixhardcodeslocal.machineName = "atlas"while serving three system doubles; the Linux desktop configs mislabel their telemetry.home.nixis now a pure desktop profile with no machine identity; atlas moved tohome-manager/hosts/atlas.nix. Flake outputs unchanged.- Correction: the Linux configs are real corporate hosts. A first pass named them
portability-<system>on the reasoning that Hostname Naming Convention’s fleet table lists no Linux machine. That table is a naming policy whose scope clause excludes work-issued hardware; I read the exclusion as non-existence. The enterprise host this whole project note reserves the private-flake pattern for already exists and is already running these dotfiles via a rebased copy — the very workflow theoverrideAdd/overrideRemovehooks inpackages.nixwere built for. Worth carrying forward: the “enterprise host blocked on a concrete machine” line elsewhere in this note is also stale. - Identity for those hosts comes from
home-manager/hosts/local.nix: absent upstream, committed on the branch each machine tracks. Upstream never touches the path, so it survives a rebase without conflicting and needs no rerere, unlike the tracked override arrays. Falls back tounconfigured-<system>at mkDefault when absent — greppable, colliding with nothing, versus the old behaviour of silently claiming to be atlas.hosts/local.nix.exampledocuments it. - The sidecar must not be gitignored. A flake’s source is the set of git-tracked files, so
builtins.pathExistscannot see an ignored path. It still resolves undernix eval --impure, which is what makes this easy to miss: that is not the pathhm switchtakes. Confirmed both directions before committing. mkHomenow takes a list of entry modules, composing a host as[profile, host]rather than a host file thatimportsthe profile. Not stylistic: the wrapper form evaluates the host before the profile it imports, reordering every list-valued option the profile contributes to.home.packagesis a buildEnv input list, so the obvious shape rebuilt atlas for a byte-identical set of 101 packages. Caught only becausedrvPathmoved when it should not have; the surface diff showed files and activation identical and packages a pure reordering. Nesting a module is not free.- Not done:
home-server.nixstill mixes profile and identity for athena, because its curated import list is the server profile. Splitting it is a larger change and would want aprofiles/server.nix+hosts/athena.nixpair.
- Correction: the Linux configs are real corporate hosts. A first pass named them
- Consider moving
local.telemetry.*intomachine.nixso thelocal.*tree has a single owner (home-server.niximports no telemetry module, so those options do not exist on athena at all). - Extend contract coverage to
local.devTools.*, which has no false-state test yet. - Decide the secrets story for the corp hosts, now the only live argument left for the private-flake pattern. Compare downstream-only sops-nix/agenix paths (same shape as the
hosts/local.nixsidecar, no new flake) against downstream-as-input. Identity is already solved; do not re-solve it.
Update 2026-07-20 — lib.uv refactor + athena pinned to 26.05 (both on main)
Two flake-architecture changes landed, from a discussion that started on where this module set sits relative to AOP / FOP / dendritic. See 2026-07-20 lib.uv Refactor and athena 26.05 Pin.
uvLib was a hidden transitive dependency; now lib.uv.mkProject (commit 7b40430). The uv2nix builder was injected via _module.args.uvLib: python.nix set it under mkIf local.python.enable, base.nix defaulted it to null, consumers guarded on hasUv = uvLib != null. A module’s value depended on whether python.nix was imported — an edge written nowhere. claude.nix dereferenced it unconditionally, safe only because the one host with python.enable = false also happened not to import claude.nix (the same masking pattern as the 2026-07-19 desktopApps bug). Moved the builder to nix/lib/uv.nix, exposed as lib.uv.mkProject by extending the module-arg lib at the flake boundary in mkHome (the EmergentMind/Misterio lib.extend convention; HM re-extends the passed lib with lib.hm and .extend composes, so modules see both). The real fix is decoupling the builder from the capability flag: building a local tool is a pure function of flake inputs, orthogonal to provisioning the Python stack, which local.python.enable had conflated. hasUv is gone; generateCheckUpstreamIssuesCompletion (a uv run tool needing the CLI at runtime) now gates on local.python.enable specifically. atlas + athena drvPath byte-identical, so neither rebuilds. This reverses the _module.args.uvLib decision recorded in ADR 0003.
athena (x86_64-darwin) pinned to nixpkgs-26.05-darwin (commit 20d7075). nixpkgs-unstable dropped x86_64-darwin (Intel Mac sunset), so the shared unstable nixpkgs now aborts instantiating that system and athena was frozen. Pinned only x86_64-darwin’s nixpkgs to the maintained 26.05-darwin branch (last release supporting the platform, tip current), selected per-system in perSystem and the base16 helper.
- Not a matched HM pin. A first pass added
home-manager release-26.05; eval failed onprograms.fzf.changeDirWidget does not exist— these modules use HM-master-only options, so pinning HM back breaks the config rather than fixing it. HM stays on master; only nixpkgs moves, the minimal delta from athena’s prior working env. - Not a frozen unstable SHA. 26.05-darwin is maintained (tip dated today) and is what nixpkgs’ own deprecation warning recommends.
- HM-master-vs-26.05 skew surfaced one assertion (HM defaults
enableNushellIntegrationon, asserts fzf >= 0.73; 26.05 ships 0.72). Nushell is unused, now explicitly off. - Eval-clean is not build-clean. x86_64-darwin evaluates end-to-end, but it cannot build on the aarch64 host, so build-time skew in the unstable-following inputs (stylix, the uv2nix/pyproject trio) built against 26.05 pkgs can only surface on athena’s own
hm switch. That switch is the outstanding validation; expect a harmlesshome.enableNixpkgsReleaseCheckwarning for the intentional HM/nixpkgs mismatch.
Update 2026-07-20 (later) — capability taxonomy: python.tools decomposed by purpose
Two more commits, from the same conversation turning to the capability model itself. See 2026-07-20 Capability Taxonomy - python.tools Decomposed by Purpose.
The smell: local.python.tools gated seven uv-installed tools (jupyter/ipython, comfy-cli, pyzotero, scalene/memray, claude-code-transcripts) whose only common trait was the uv tool install mechanism. That is a mechanism-grouping masquerading as a capability — purpose scattered across the install axis. Also audited how much axis 1 actually buys: of 42 top-level modules, the curated import list is load-bearing for only ~9 (copier/docker/media-stack/… that would activate if auto-discovered on the server); the other 14 desktop-only modules are redundantly gated by a capability flag or a default-off enable. That is the concrete cost of a future dendritic axis-1 collapse — those 9 need explicit guards first.
Commit d0ae738 — split by purpose, mechanism stays shared. local.dataScience.enable (jupyter/ipython + kernels), local.devTools.profilers.enable (scalene/memray, a 4th devTools subgroup), and python.tools shrunk to the residual. python.nix partitions the tool set into purpose groups; the activation installs the union of the enabled ones. Key-sorted iteration keeps all-on byte-identical — atlas + athena drvPaths unchanged.
Commit 8194f26 — dissolve the residual. claude-code-transcripts removed outright; comfy-cli and pyzotero became individual programs.comfy-cli.enable / programs.pyzotero.enable opt-in flags (declared in python.nix, default off, enabled in the desktop profile per an AskUserQuestion — desktop+corp keep them). local.python.tools deleted entirely (option + assertion + server-profile line). atlas drvPath changed by exactly the cc-transcripts delta (the other six tools verified unchanged); athena untouched. Each new flag installs via the uv stack, so each got a -> local.python.enable assertion — the no-dependency-resolution rule again: a cross-capability requirement has only a failing assertion, no native satisfaction.
Net: python.tools (a seven-tool mechanism-bag) is now four purpose/program-scoped gates, with the uv-install machinery still DRY. The capability surface expresses host intent rather than install plumbing. Contract-test caveat: only dataScience deploys a checkable artifact; the other three install via activation content with no surface, documented inline.
Next Actions (2026-07-20)
- Run
hm switchon athena to validate the 26.05 pin end to end (build-time, not just eval). Drive it over SSH per the athena bring-up notes. If a package fails to build, it will be a stylix/uv2nix-vs-26.05 skew, fixed per-package. - Desktop
hm switchwill now rebuild once (small): the athena pin left atlas byte-identical, but removing claude-code-transcripts changed atlas’s installUvTools script. Expected, one-time. - Optional:
home.enableNixpkgsReleaseCheck = falseon the server profile to silence the deliberate HM-master-on-26.05 mismatch warning. -
hm switchon the desktop is a no-op (drvPath unchanged), but confirm on next routine switch.