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.
Update 2026-08-04 — uv project registry; lib.uv retired; devTools gate closed
Five commits, merged to main by fast-forward (branch deleted); not yet
pushed — main is ahead of origin/main by 5. Opened as a question
about moving the Python build trees under pkgs/ with a per-package enable flag;
two of those three ideas did not survive. See
2026-08-04 uv Project Registry and the devTools Gate.
Two of the five are follow-ups from review: 2ef6930 fixes the root flake’s
pre-commit failure (see Next Actions below — the diagnosis in the first pass was
wrong), and 338d009 corrects a claim I had put in the registry header.
I had written that obsidian-mcp-proxy “could be built by a module and tested by
nothing.” It is tested, by tests/obsidian-mcp-bridge, and always was — that
check landed in the same commit that created the project (0812ce3). Its absence
from the old literal list was not neglect either: the list predates the project
by a month. Nothing had drifted, so the comment was naming a failure that never
happened to justify a change that stands on its own.
The move-under-pkgs/ half is rejected. pkgs/ holds seven third-party
build recipes (600-2000 bytes each, source pinned by flake = false inputs);
the uv projects are first-party workspaces of 45-56 tracked files. And
files/scripts/ is one deploy tree — shell.nix:420 string-appends into it for
~/bin, plus lib/scripts.nix, tmux.nix, sqlite-seed.nix, signoz.nix,
lib/agent-state.nix — so moving only the Python subdirectories splits it for
nothing. The derivations never lived there either way.
The per-package-flag half is rejected on the grounds already recorded here.
Dendritic lists boolean enable options as an anti-pattern; the 2026-07-20
entry framed that as the tax auto-discovery charges. The right lever was the
flag that already exists.
Commit b55df95 — nix/lib/uv-projects.nix names the five projects once.
Previously implied rather than declared: four modules each called
lib.uv.mkProject on a relative path, and tests/uv-projects.nix carried a
literal list of four names plus a second copy of the pythonSet assembly from
lib/uv.nix. The duplicate build was the part that mattered — a build-system
override or sourcePreference change had to be made in two files to take
effect, so a suite could pass against an environment the shipped package never
used. The registry feeds perSystem.packages, so easyOverlay carries each to
pkgs.<name> and nix build .#claude-ops works, matching ./pkgs
reachability; checks now receives the built packages, not a second name
list. It throws in both directions (unregistered uv.lock on disk, registered
project gone), both branches tested with a probe directory before wiring in.
lib.uv is retired — the centerpiece of 7b40430 six weeks ago. Once every
call site resolved through pkgs.<name>, the lib.extend in mkHome had zero
consumers. That refactor was the correct fix for ambient _module.args.uvLib
injection; the registry subsumes the problem rather than reversing the judgement.
Commit 15395f6 — claude-ops and gh-review-preview now gate on
local.devTools.enable. They deployed on every host importing shell.nix,
athena included, so a headless Intel server built duckdb and bashlex for a
session-analysis tool with no sessions. This completes the 2026-07-20 story
rather than correcting it. That change decoupled the builder from
local.python.enable, which was right, and left the tools gated on nothing —
and the comment explaining the half it had solved (self-contained venvs need
no uv CLI) made the remaining gap read as deliberate. python.enable answers
whether a host can run them, never whether it wants them. Contract test
written first; failed on all three surfaces before the guard existed.
Resolves two open Next Actions above:
-
Decide: guardBoth, effectively. There is nouvLibinclaude.nix, or narrow thelocal.python.enabledescription.uvLibleft to guard, andmachine.nix’s description no longer claimspython.enablecovers the uv2nix-built tools.home-server.nixcarried the same false claim and is corrected too. -
Extend contract coverage toDone, covering both packages and the completion activation.local.devTools.*, which has no false-state test yet.
Also deleted files/scripts/bash-command-validator: 690 files / 33 MB of
.venv and caches with zero source files, orphaned when the project’s
source was removed.
atlas drvPath byte-identical at agxwsrv407cyl33fbjbxvglcvjrh4v7i across
every step including the consumer conversion — which is what proved the
registry-built packages are the same derivations, rather than merely suggesting
it. nix flake check green; athena evaluates and now carries neither tool.
Correction to the dendritic cost estimate recorded above. The 2026-07-20
framing (and my own reasoning this session) assumed auto-discovery collapses
axis 1. It does not: import-tree auto-imports the aspect definitions, while
hosts still choose which aspects to import. Dendritic would preserve the
curated import list, not eliminate it. That removes the mechanism this note has
twice cited as dendritic’s main advantage here — collapsing axis 1 so no
composition boundary can mask a dead flag. Worth re-reading the 2026-07-19 and
2026-07-20 dendritic passages with that in mind.
Decision: not adopting dendritic now. ~50-module restructure against a
config that works and is documented by ten-plus ADRs. Revisit when a NixOS host
starts sharing features with the Darwin hosts; hosts/obsidian-mcp is that host
today and shares nothing.
Refinement (2026-09-04): auto-discovery forces an inert default, not a
boolean. The 2026-07-20 claim that dendritic’s no-enable stance is
“incompatible with auto-discovery by construction” is too strong. A module
whose config is mkIf (declaration != []) is inert when auto-imported and
active once a profile or host declares something, the way home.file and
launchd.agents already behave. programs.homebrew (commit ae0d70b) is the
first module built this way: no enable, gated on a non-empty
taps ++ brews ++ casks, with an empty-declaration fixture in
tests/homebrew.nix proving inertness. A boolean is still right where “off”
must survive a declaration (litellm on athena) or where a capability flag
needs a handle. See 2026-09-04 The Brewfile That Passed Because It Was Empty.
Next Actions (2026-08-04)
-
MergeDone, fast-forward, branch deleted. Re-verified onrefactor/uv-project-registryintomain.mainafter the merge: root andnix/nix flake checkboth green,just checkgreen, athena evaluates. atlasdrvPathstillagxwsrv407cyl33fbjbxvglcvjrh4v7i, unchanged from before any of this work — so the desktophm switchis a confirmed no-op. - Push
main(3 commits ahead oforigin/main; the five branch commits reached the remote already). -
packages.<system>.<uv project>is now defined for all four systems including x86_64-darwin. Evaluation only andnix flake checkis green, but confirm this does not reintroduce what excluding athena fromcheckswas avoiding. -
Pre-existing and unrelated: the root flake’sFixed (2ef6930), and it was neither a skew nor unrelated to this repo’s shape. Ruff’s isort infers first-party by resolving againstpre-commit-runfails onnix/tests/ipython-config/test_ipython_config.py(ruff skew against theprekthatjust checkruns).src, which defaults to the project root — and this repo’s root is~/.config, which carries a gitignoredipython/directory (the IPython user config home-manager deploys). APFS is case-insensitive, soIPythonresolves first-party in a working tree; a flake sees only git-tracked files, so it resolves third-party in the sandbox. Same ruff, same config, opposite answers, and the file could not satisfy both. Fixed by declaringknown-third-party = ["IPython"]in aruff.tomlscoped to that test directory. This is a structural hazard of rooting the repo at~/.config: every gitignored directory there (nvim/,zsh/,nix/, …) is a candidate first-party module name for any tool that infers from the filesystem. - athena
hm switchstill outstanding from 2026-07-20; now also validates that it drops both tools. - Consider whether other loose
.pyfiles carry the same latent isort ambiguity. OnlyIPythonerrors today; the collision class is structural.
Update 2026-08-04 (later) — the last PEP 723 scripts packaged, and their lint debt paid
Three more commits on main, continuing the same session. Same journal entry:
2026-08-04 uv Project Registry and the devTools Gate.
Commit 052b576 — deleted claude-with-timeout. An abandoned prototype
whose body says # this is conceptual - actual detection TBD, with a FIFO
written by a monitor subshell and read by nobody. Read before removing.
Commit d4256a1 — arxiv-mcp-server, acl-mcp-server and
check-upstream-issues became registered uv projects. They were PEP 723
scripts run via uv run --script, which re-resolves the dependency tree from
PyPI on every invocation: for the two MCP servers that means the server needs
network to boot, its version set drifts silently, and nothing can be built or
exercised in a sandbox. mcp.nix now launches them from the store. Generated
from the house copier template (gh:achhina/python-project-template v1.8.0)
rather than hand-rolled — the convention was discoverable from the
.copier-answers.yml every existing project already carries. Sources moved
with git mv; all three picked up pytest checks automatically because the
template generates tests/ and the registry keys its check derivation off
pathExists src/tests.
Two corrections from that work worth keeping: the acl-mcp-server dependency
conflict I reported was self-inflicted (requires-python = ">=3.11" — uv lock resolves universally across every interpreter in the declared range,
so one unsatisfiable Python fails the whole lock), and adding lxml-stubs to
fix one ty error produced 53 more, because the stubs type xpath() as a wide
union needing casts at ~50 sites.
Commit a4a6684 — the packaging commit’s per-file ignores were debt, and are
now paid. Holding those scripts to the template rule set for the first time
surfaced 16 violations, and the packaging commit suppressed all of them in one
per-file-ignores block under one comment. That block mixed two different
things: RUF029/PLW0603/PLC0415 describe how an MCP server is shaped
(coroutine tool handlers, a module-global rate-limit clock, a deferred
heavyweight import), while A002, B904, E501, N818, PLR0911,
PLR0912, PLR0917 and PLW0717 were unpaid work. In TOML the two are
indistinguishable.
Fixed rather than suppressed. arxiv: Error-suffixed the two private S2
exceptions, chained the exhausted-retry raise with from err, renamed
daily_papers’ format to feed_format, and made harvest_metadata /
s2_search take their optional filters keyword-only (free, since MCP and click
both already pass by name). check-upstream-issues: format_time_ago went from
a 13-statement try to one narrow try plus a shared pluralizer, and main’s
per-issue loop moved out to collect_statuses.
Both refactored functions got a characterization suite first; the 16
format_time_ago cases passed against the old implementation before the
rewrite. Then cleared the ignore table entirely and re-ran to confirm every
surviving entry still fires — all six do, and each now carries its own reason.
Worth remembering: ruff reports an unused noqa, but never an unused
per-file-ignores entry, so nothing tells you when a suppression goes stale.
All gates green (just check, root and nix/ nix flake check, each
project’s own just check); the three store binaries were run directly and the
three changed CLI commands exercised end to end. athena still carries none of
the five devTools packages.
Commit bb191d8 — the last suppression, the lxml one, is gone too. It was an
inline ty: ignore on the lxml import, justified by a comment claiming
lxml-stubs would need a cast at ~50 call sites. Wrong in shape: all 48
diagnostics land in one contiguous parsing block routing through three xpath
result shapes, so three narrowing wrappers (_xs scalar, _xstrs string list,
_xels element list) retire every one. lxml-stubs sits in the type-check
group, verified absent from the built store path’s closure. The parsing core
had no coverage and this rewrote it, so it now has a fixture-XML suite over all
four parsers.
Commits dda09a3 and 25f547e — the MCP servers’ three “structural” ignores,
checked rather than restated. Only PLC0415 survived.
RUF029was waived on the claim that the handlers “cannot become sync without changing how they are registered.” fastmcp registers sync tools, andFunctionTooldefaultsrun_in_thread=Truefor them. The waiver was hiding a defect: declaredasyncwith noawait, eight tools ran blocking work on the event loop (acl’s six all call_load_anthology()). Converted;_rungained anisawaitablebranch, the only thing that coupled them toasync.PLW0603is real shared state, which argues for naming it rather than rebinding a module global from a coroutine. Both servers use a_RateLimitStatedataclass; the Anthology memo becamefunctools.cache.- The audit also found
_load_anthology()called at module scope, so importing acl’s cli loaded the corpus and every subcommand paid —--helptook 8.7s, now 1.3s with the load moved intoserve. - 25f547e retires acl’s file-level
ruff: file-ignore[blind-except, try-except-pass], which was written for_paper_to_modelbut also covered a blind except 280 lines away. All best-effort reads funnel through one_best_effortguard, with tests for the degradation contract.
Suppression inventory across the three projects, after all of this: one
inline suppression, on _best_effort; per-file ignores are the template’s test
and docs/conf.py defaults, plus PLC0415 per MCP server, three subprocess
rules in check-upstream-issues (S404/S603/S607), and PLC2701 on arxiv’s
and acl’s tests so they can import module-private helpers. Every one verified
load-bearing by clearing the table and re-running; each carries its own reason.
Commit b6d63e5 — the author/papers failure, fixed in both the data and
the check. ~/.acl-anthology-data was a February checkout 5108 commits
behind, from before upstream replaced data/yaml with data/json. The person,
venue and SIG indices each read a file under json/ and are built lazily, so
the bad load succeeded and only three of six subcommands raised, at first use.
Correcting an earlier note in this entry: from_repo does not build
indices the disk branch skips — it is clone_or_pull_from_repo plus the same
Anthology(datadir=...). The difference is the pull. That made the disk
branch a trap: gated on data/xml alone, it accepted the half-populated
checkout and then never updated it, so one upstream layout change froze it
broken indefinitely. _data_is_complete now names every directory the reader
opens and falls through to from_repo otherwise, which repairs. The local
checkout is fast-forwarded (ea3e49a → 3bbf46af) and all six subcommands are
verified against the store binary.
Update 2026-08-04 (later still) — uv projects moved to nix/projects/
Commit d44c48e. Reopens and reverses the “leave them in files/scripts/” half
of this morning’s decision, on the evidence rather than the argument.
files/scripts/ had become two unrelated things under one name: ~20 loose
scripts plus tmux/, read by path from shell.nix, tmux.nix, glance.nix,
sqlite-seed.nix, signoz.nix, packages.nix, claude.nix and agent-state.nix; and
eight uv projects, read by path from exactly two lines (the registry root
and the obsidian-mcp-bridge test). The rejection rested on “it is one deploy
tree, splitting it gains nothing”, which is true of the scripts and was never
true of the projects — they reach modules as pkgs.<name> through the overlay.
They also sat under home-manager/files/, which means “file sources for
home.file”, something they never participate in.
Now nix/projects/, a sibling of pkgs/ and lib/. The original pkgs/
proposal stays rejected for the reason recorded this morning: that directory is
third-party build recipes over flake = false inputs. nix/AGENTS.md now
carries a table stating which of the three homes new code goes in, so the next
reader does not have to reconstruct this.
Five of the eight package derivations are byte-identical across the move —
the proof it is inert. The three that changed are files edited alongside it:
_run’s type annotation in both MCP servers, and a gitleaks inline allow in
claude-skill-sync. athena’s activation drvPath is unchanged; atlas’s moves by
exactly those three.
Latent problems the move surfaced, worth remembering as a class:
shell.nixstill claimed the~/binpython scripts declare deps in PEP-723 headers — false since the morning’s packaging.- The same pinned
ty0.0.65 typed_rundifferently in-project vs in the prek hook, because they resolve against different environments. Fixed by making the type not depend on that. - gitleaks flagged PBKDF2 test vectors that had never been inside a staged
diff, since
prek runonly sees staged files. A green gate says less about the tree than it appears to.
Update 2026-08-04 (last) — files/scripts/ is now shell-only
Commits 254126b and 0c95ea2. Closes the packaging arc: every first-party Python
tool in this repo is a locked uv workspace under nix/projects/, and nothing in
files/scripts/ is deployed as a raw file source any more.
The question was whether to move everything and delete scripts/. No — the
inventory settles it. Of 21 tracked entries, 19 are read by path into
derivations or spliced into Nix strings; a bash file handed to
writeShellApplication has no workspace to lock and cannot get a registry
entry, since the registry’s on-disk test for “is this a project” is a committed
uv.lock. Moving them would restore the two-things-one-name problem d44c48e
just fixed.
The remaining two — the only actual home.file sources in the directory —
were parse-history and parse-claude-tools, uv run --script files with PEP
723 headers. Packaged. Their dependencies were unpinned and unbuilt, so
parse-claude-tools was one release of claude-code-log away from breaking
with nothing in nix flake check to catch it — and it never imported
claude-code-log at all. Both now gate on local.devTools.enable, which
removes them from athena; parse-claude-tools reads the same
~/.claude/projects transcripts as the already-gated claude-ops, so this
extends a decision rather than making one.
Two structural findings, both about gates that were not looking:
.gitignorehad an unanchored.pre-commit-config.yaml, meant for the root config git-hooks.nix generates. It also matched every per-project config, and prek discovers a nested project only when its config is tracked — soprek listreported six projects where the directory holds eight, andjust checkreturned 0 across two projects it never opened. The six that predate this were force-added past the pattern one at a time, which is why the pattern itself stayed wrong. Anchored. This is the repo’s second unanchored-pattern incident.- The template diverges from all nine projects in two places. At the same
_commit: v1.8.0, every existing project had hand-fixed.envrc(a shellcheck directive that does not actually silence SC1091 without-x) andflake.nix(an unusedselfin the outputs lambda). Both fixes were reapplied a ninth and tenth time here. Worth pushing upstream togh:achhina/python-project-template, sincecopier updatewill keep reintroducing them.
nix/AGENTS.md corrected: it described files/scripts/ as deploying via
home.file, the house rule for 2 of 21 entries.
Then renamed the directory itself (b3ad2cd): home-manager/files/scripts/ →
nix/scripts/, a sibling of pkgs/ and projects/. With the last two file
sources packaged, home-manager/files/ described none of its contents. The
eight entries that remain under the old path — tridactylrc, dashboards,
the atuin seed history — are what the name is actually true of.
Verified inert by diffing closures rather than assuming: athena byte-identical,
and atlas’s single difference is the SigNoz alert annotation naming
patch-otel-config.py, a doc pointer inside an alert body. That will churn the
alert on the next tofu apply, which is expected. All nine ~/bin derivations
rebuild and resolve.
ADR references under docs/decisions/ deliberately left stale — a dated record
is not a live pointer, and 0004 had already carried a stale scripts path since
d44c48e.
Update 2026-09-04 — athena drops two uv projects (the Intel wheel cliff, second instance)
Commit 81e1e15. acl-mcp-server and arxiv-mcp-server are now excluded from
perSystem.packages on x86_64-darwin. They reach pymupdf4llm, then
pymupdf-layout, then onnxruntime, whose locked 1.28.0 publishes neither an Intel
macOS wheel nor an sdist. Unlike the cryptography case from July there is no
lock to fork and no sdist to source-build, so dropping the two projects on that
one system is the entire fix.
Safe because of two properties this project already established: home-server.nix
curates its imports by hand and does not import
coding-agents/common/mcp.nix, and checks maps over hostHomeConfigurations,
which deliberately omits x86_64-darwin.
The trap worth recording is the gate. The first attempt keyed on
pkgs.stdenv.hostPlatform.isx86_64, which also matches x86_64-linux. Those are
the corp workstations, they run the full desktop profile, and mcp.nix puts both
servers in home.packages unconditionally, so that gate would have broken a
deployed host with a missing-attribute error while leaving the Darwin problem
untouched. A platform exclusion in this flake belongs on the full system
double, not on an arch or OS predicate: isx86_64, isDarwin and isLinux
all read as safe narrowings and each silently covers a host you were not
thinking about. Verified by evaluating
homeConfigurations."x86_64-linux".config.home.packages rather than by reading.
Two related fixes in the update pipeline the same day (1b45d0d): nix flake check ./nix resolved against the caller’s cwd rather than the config being
updated, and the flake-input count in the commit body doubled every input
because a unified diff carries both the removed and the added "lastModified"
line.
Verification note: the pytest suite under nix/tests/update-changelogs/ was
green against a flake.nix that would not parse, because those tests assert on
the text of the update script. The instrument that would have caught it is
nix flake check, and the change under test was to the script that runs it.
Next Actions (2026-09-04)
-
hm switchon athena to confirm the exclusion actually unblocks its build; 2026-09-04’s verification was evaluation-only from atlas. - Consider a check that evaluates every system’s package set, so a parse or
attribute error in
flake.nixcannot ride in behind a green pytest run. - Restore both projects if onnxruntime ships an Intel macOS artifact again.