2026-07-19 Capability Flags Were Never Tested
What I set out to do
Review the local.* capability model landed 2026-07-04/05 (see Dotfiles Host Profiles via Private Flake and 2026-07-04 Home Manager athena Server Profile): enumerate the top-level opt-ins, check them over, and pull up whatever design artifacts existed in the vault and the repo’s ADR log.
What I actually did
Mapped the model (local.profile, desktopApps, devTools, python, releaseCooldown, telemetry, plus the machineName / githubUsername / trusted identity options), then found the gap that mattered.
local.desktopApps.enable was inert. Read in packages.nix:189 and nowhere else. tridactyl, zotero, sillytavern deployed unconditionally; aerospace and bettertouchtool gated on isDarwin only. jupyter and ipython config deployed regardless of local.python.tools.enable. The flag’s own description promised it gated the desktop modules. It gated a package list.
The root cause is a testing gap, not carelessness: checks builds only the desktop profile, where every flag is true, so a flag that gates nothing is indistinguishable from a flag that works.
Fixed test-first. Wrote nix/tests/capability-flags.nix, which re-evaluates the desktop config with each flag off via extendModules and asserts the artifacts disappear. Confirmed red (5 leaks), added lib.mkIf guards to the seven modules, confirmed green. Also added assertions for the machineName = "localhost" silent-wrong-value default and the python.tools without python.enable half-state, and wrote ADR 0021, the first written record of the model outside this vault.
What was striking
My first version of the test passed while asserting nothing. I keyed it on home.file attribute names. But xdg.configFile entries land under an absolute key (/Users/achhina/.config/tridactyl/tridactylrc) while home.file entries keep a relative one, so the tridactyl contract matched nothing and reported clean. This is the same absolute-key drift the 2026-07-01 HM bump caused in settings.json (see 2026-07-09 Claude Settings Hardening and checkLinkTargets Root Cause). .target survived both changes; the attribute name did not.
I only caught it because tridactyl was conspicuously absent from the red-phase failure list. So I added a second guard to the test: any contract target missing from the baseline config is itself a failure. A test that cannot fail is worse than no test, and this one nearly shipped.
A documented rationale can outlive its evidence. The curated import list in home-server.nix is justified by Intel breakage, but the July bring-up found zero Intel errors across the full closure. The arrow-cpp breakage was real, so the axis-1 decision still stands, but its stated justification is weaker than it reads. Recorded in the ADR rather than quietly acted on.
Also found: local.python.enable = false is unsupported on the desktop module set. claude.nix dereferences uvLib unconditionally, where shell.nix correctly guards via hasUv != null. The option description over-promises. Left as a known limitation.
Verified atlas’s activationPackage.drvPath unchanged (zvdqhbqmm3k9…) via a worktree at HEAD rather than stash-and-diff, avoiding the git stash pop unstaging footgun. just check and nix flake check both clean.
Later the same day: extending coverage to the heavy modules
Pushed the contract test out to litellm, open-webui, and signoz — the three the curated import list exists to protect. Two findings.
programs.litellm.enable did not exist. The module’s config was unconditional. Only omission from home-server.nix’s import list kept the arrow-cpp puller off athena, and any host importing the module got the proxy regardless. The 2026-07-04 note called this “the one required module refactor.” It never landed, sat there 15 days, and no check could see it. The test found it by failing to evaluate: the contract named a flag that wasn’t there.
I had already shipped a broken test. The contracts listed Darwin-only paths unconditionally, so the Zotero entry was vacuous on Linux and my own baseline guard failed aarch64-linux and x86_64-linux. I never saw it because nix flake check skips incompatible systems by default and I only ran Darwin — the same class of mistake as the original bug, one layer up. A guard I wrote specifically to catch vacuous contracts was itself only running on a third of the matrix. Contracts are platform-split now, and Zotero asserts the Linux config root rather than dropping coverage.
Also generalized the test over four surfaces instead of just home.file. litellm and open-webui deploy through launchd agents and activation scripts, so a home.file-only check would have called them gated while the proxy kept running.
The upshot: the dendritic revisit trigger I wrote this morning is already met. I’m deliberately not calling that settled — the residual case for the import boundary is narrower than it was (contracts assert artifacts vanish, not that the closure is Intel-clean), but “narrower” is not “gone.”
Third pass: the machineName hardcoding
home.nix set local.machineName = "atlas" while flake.nix built it for three system doubles, so both Linux configs also claimed to be atlas. Since machineName feeds the OTel host.name resource attribute, that’s a collision waiting in any SigNoz view grouped by host.
I checked Hostname Naming Convention, saw the fleet listed as atlas, athena and a pending mbp-2010 — all Darwin — and concluded there was no Linux machine, so the two Linux entries must be portability fixtures rather than hosts. I named them portability-<system> and wrote that framing into a commit message, the ADR, and this journal.
That was wrong, and Adam corrected it: the Linux configs run on corporate machines, where a copy of the dotfiles is kept and rebased. The convention note says its scope is personally-owned machines and explicitly excludes work-issued hardware — I read “excluded from the naming convention” as “does not exist.” The one source that could have corrected me was the source I used to justify the error.
Worth sitting with: the fleet table was evidence about naming policy, not an inventory. I treated a scoping clause as an existence claim, and the resulting design was coherent, well-commented, and built on a false premise — which is exactly the kind of wrong that survives review.
Corrected: home.nix is a pure profile with no identity, atlas’s identity lives in hosts/atlas.nix, and the Linux hosts get identity from an untracked-upstream hosts/local.nix sidecar committed only on the branch each machine tracks. Upstream never touches that path, so it survives a rebase without conflicting — no rerere needed, unlike the tracked overrideAdd/overrideRemove arrays in packages.nix. Absent, it falls back to unconfigured-<system>: greppable, colliding with nothing.
The sidecar must not be gitignored. I gitignored it first, which silently broke it: a flake’s source is the set of git-tracked files, so builtins.pathExists cannot see an ignored path. The tell was that it still resolved correctly under nix eval --impure — the path that does not deploy — and returned unconfigured-x86_64-linux under the pure flake ref hm switch actually uses. Testing only the convenient path would have shipped it.
The interesting part was a self-inflicted rebuild. The obvious shape is hosts/atlas.nix doing imports = [ ../home.nix ]. That changed atlas’s drvPath, which should have been inert. Diffing the config surfaces showed home.file and home.activation byte-identical and home.packages a pure reordering of the same 101 packages. A wrapper module evaluates before the profile it imports, so every list-valued option the profile contributes to shifts position, and home.packages is a buildEnv input list whose order is hashed.
Fix: compose profile and host as siblings (mkHome system [profile host]) rather than nesting. drvPath returned to zvdqhbqmm3k9…. Same split, no rebuild.
Worth keeping generally: imports is not free. Nesting a module changes merge order, and for list-valued options that is a semantic change to the output hash even when the set is identical. Sibling composition preserves position; wrapper composition does not.
Top 3 tomorrow
- Decide on the
claude.nixuvLibguard vs narrowing thelocal.python.enabledescription. home.nixhardcodesmachineName = "atlas"across three system doubles; the Linux desktop configs mislabel their telemetry.- Consider whether
local.telemetry.*should move intomachine.nixso thelocal.*tree has one owner.