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 plus local.profile cover the role axis (2026-07-04/05), and hosts/local.nix covers 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 — inline overrideAdd / overrideRemove arrays relying on git rerere to absorb rebase conflicts.
  • nix/home-manager/modules/coding-agents/claude/claude.nix:46-60 — sidecar claude-overrides.nix loaded via builtins.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-parts per-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 exposing mkHomeConfigurations per 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 existing overrideAdd / claude-overrides.nix hooks are sufficient for now. Answered 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.
  • 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.nix sidecar pattern should be generalized or retired once the private-flake approach is in place.
  • Smaller stepping-stone option: refactor one module (e.g. git.nix or claude.nix) to declare options.dotfiles.<feature>.* with mkEnableOption / mkPackageOption, gated via lib.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-confighosts/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.enable gated 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 missed enable guard reaches the x86_64-darwin build. That cost is falling: Intel breakage measured at zero errors across the full closure (twice), and tests/capability-flags.nix now 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 just desktopApps / 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.enable did not exist at all — the module’s config was 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.

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).
  • enterprisework via 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 unless home.nix turns them on → not athena blockers.
  • modules/litellm/default.nix has NO enable option and NO mkIf gate — unconditionally active on Darwin, and the direct arrow-cpp puller. Adding programs.litellm.enable and wrapping its config in mkIf is the one required module refactor. Caveat: the options block must stay ungated because jupyter.nix and sillytavern.nix read config.programs.litellm.port / .defaultClaudeModel; only the config (packages, launchd agent, files) gets mkIf.
  • modules/python.nix (uv2nix venvs incl. jupyterlab) and shell.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 build on athena; each failure reveals the next path to gate or overrideRemove (the hook at packages.nix:6-12 already supports per-host removal). Static reading cannot enumerate it. Recommended de-risking order: prototype athena with a curated-import entry point (bypass the home.nix auto-discovery, list only the safe core), get it building, then convert the survivors to dotfiles.<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 both home.nix (desktop) and the new curated home-server.nix (server profile).

  • Admitted x86_64-darwin to the flake systems (server profile only); refactored homeConfigurations into a mkHome helper + homeConfigurations."x86_64-darwin" -> home-server.nix; kept checks desktop-only.

  • shell.nix: ghostty gated off on x86_64-darwin (no prebuilt, headless); uvLib made optional. Footgun found: a formal-level uvLib ? null default does not work — the HM module system eagerly passes every declared function arg, shadowing the default. Fix: base.nix sets _module.args.uvLib = lib.mkDefault null, which python.nix overrides on the desktop.

  • Verified on atlas: activationPackage.drvPath byte-identical before/after (pure refactor, gjlka2k6…); athena homeConfigurations.x86_64-darwin evaluates (9zc85ann…) so no broken-marked packages in the curated closure; just check passes. 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-darwin with zero Intel breakage. The feared package whack-a-mole never materialised; 0 errors across the full closure. The only cost is time: cache.nixos.org has 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, so trusted-users doesn’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 ~/.config as the repo in place (gh/secrets gitignored, no clobber); extra-trusted-users = achhina in /etc/nix/nix.conf; first activation via nix run home-manager/master -- switch --flake ~/.config/nix#x86_64-darwin -b backup.

  • packages.nix trimmed (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 real dotfiles-style capability flag (lives under local.* for now).

  • Two fresh-bootstrap gotchas found + fixed: (1) non-interactive SSH has no nix on PATH and flakes disabled → drive with the full /nix/var/nix/profiles/default/bin/nix path + NIX_CONFIG='extra-experimental-features = nix-command flakes' + exported XDG_*. (2) mkZshCompletionGen wrote to $XDG_DATA_HOME (runtime env) under set -u; unbound on a bare-shell bootstrap → aborted activation. Fixed by baking config.xdg.dataHome at 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 into main and pushed.
  • Second capability flag — local.devTools (commit 4badaf5). Added a dev-toolchain master switch with independent languageServers / formatters / runtimes sub-toggles, following the releaseCooldown master+sub-toggle idiom already in machine.nix (each sub-toggle defaults to the master). packages.nix gates its three dev groups on these via optionals in original list position. home-server.nix sets local.devTools.enable = false → athena 87→60 packages (drops 11 LSPs + 8 formatters/linters + 8 runtimes/compilers). atlas drvPath byte-identical before/after (229c3i27…) so the desktop host does not rebuild — the optionals true xs == xs no-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.nix auto-discovery + per-module disable flags. Doing so would require a correct enable guard 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 under local.* (not the aspirational dotfiles.* namespace) for consistency with the existing local.desktopApps.enable.
  • Deployed to athena (generation 2): hm switch over 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) plus tools.enable (default = master — the heavy uv tool install set: jupyterlab/ipython/comfy-cli/pyzotero/scalene/memray, several pulling torch/Jupyter into ~/.local). Same master+sub-toggle idiom as devTools/releaseCooldown.
  • python.nix gating: programs.uv + installPythonVersions + _module.args.uvLib on python.enable; installUvTools on python.enable && tools.enable. uvLib is mkIf python.enable {...} so when disabled it falls back to base.nix’s mkDefault null and shell.nix’s hasUv gate skips the local tools correctly — no leaky flag.
  • home-server.nix imports ./modules/python.nix and sets local.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, but uv tool list is 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: generateCheckUpstreamIssuesCompletion shelled out to uv unconditionally and warned (exit 127) on the uv-less server; gated it on hasUv (commit 26428ab). With uv now present it runs cleanly anyway.
  • Verification held: atlas drvPath byte-identical at 229c3i27… across all of devTools + python (all flags default true → mkIf/optionals no-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 maps profile == "server"{ desktopApps.enable, devTools.enable, python.tools.enable } = mkDefault false; home-server.nix collapses to the single local.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 (atlas 229c3i27… via mkIf false; athena gqa9ngn8…, so the athena redeploy was a confirmed no-op, still generation 3). Adding a second server host (oracle) is now a one-line local.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) and the 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.enable gated only a package list. Read at packages.nix:189 and nowhere else. tridactyl / zotero / sillytavern deployed unconditionally; aerospace / bettertouchtool gated on isDarwin alone. Its own description claimed it gated the desktop modules. Now guarded with lib.mkIf in all five.
  • local.python.tools.enable did not gate jupyter / ipython config. Only the activation-time uv tool install was gated, so the config files deployed on a host with none of the tools. Both modules now gate on it (not on desktopApps — this is data-science tooling, not GUI).
  • Root cause is structural, not sloppiness. checks builds 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 = false is unsupported on the desktop module set. claude.nix dereferences the uvLib module arg unconditionally, where shell.nix correctly guards on hasUv != 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 uvLib in claude.nix, or narrow the local.python.enable description to match reality.
  • home.nix hardcodes local.machineName = "atlas" while serving three system doubles; the Linux desktop configs mislabel their telemetry. Done 2026-07-19 (commit fb61d08). home.nix is now a pure desktop profile with no machine identity; atlas moved to home-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 the overrideAdd/overrideRemove hooks in packages.nix were 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 to unconfigured-<system> at mkDefault when absent — greppable, colliding with nothing, versus the old behaviour of silently claiming to be atlas. hosts/local.nix.example documents it.
    • The sidecar must not be gitignored. A flake’s source is the set of git-tracked files, so builtins.pathExists cannot see an ignored path. It still resolves under nix eval --impure, which is what makes this easy to miss: that is not the path hm switch takes. Confirmed both directions before committing.
    • mkHome now takes a list of entry modules, composing a host as [profile, host] rather than a host file that imports the profile. Not stylistic: the wrapper form evaluates the host before the profile it imports, reordering every list-valued option the profile contributes to. home.packages is a buildEnv input list, so the obvious shape rebuilt atlas for a byte-identical set of 101 packages. Caught only because drvPath moved 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.nix still mixes profile and identity for athena, because its curated import list is the server profile. Splitting it is a larger change and would want a profiles/server.nix + hosts/athena.nix pair.
  • Consider moving local.telemetry.* into machine.nix so the local.* tree has a single owner (home-server.nix imports 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.nix sidecar, 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 on programs.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 enableNushellIntegration on, 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 harmless home.enableNixpkgsReleaseCheck warning 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 switch on 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 switch will 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 = false on the server profile to silence the deliberate HM-master-on-26.05 mismatch warning.
  • hm switch on the desktop is a no-op (drvPath unchanged), but confirm on next routine switch.