When macOS Tahoe breaks Nix: it's not the installer, it's BTM
After updating to macOS Tahoe, my multi-user Nix setup stopped working. nix --version returned "command not found", /nix was empty, and none of my devenv shells could start. I reached for the obvious reflex — reinstall — and almost destroyed 78 GB of Nix store in the process. The real problem was somewhere I didn't expect: Apple's Background Task Management silently blocking system LaunchDaemons.
The trap the installer sets
Running the official installer on an "already installed" system produces this:
Action `encrypt_apfs_volume` errored
The keychain lacks a password for the already existing "Nix Store"
volume on disk `disk3`, consider removing the volume with
`diskutil apfs deleteVolume "Nix Store"`
That suggestion is dangerous. diskutil apfs deleteVolume "Nix Store" irreversibly wipes the APFS volume containing your entire Nix store — every installed package, every profile, every pinned version. The installer has no idea whether you have 0 GB or 800 GB of derivations on that volume.
The installer's mental model is "new install". It's not designed as a repair tool. If you see that error after a macOS upgrade, the right move is to stop and diagnose — not to follow the suggestion.
What was actually broken
A few minutes of read-only probing painted a very different picture:
diskutil apfs list | grep -A3 "Nix Store"
# → Name: Nix Store, Mount Point: Not Mounted, 78 GB consumed
ls /nix
# → empty (just the synthetic.conf mountpoint)
ls -la /Library/LaunchDaemons/org.nixos.*
# → both plists present, correct perms, root:wheel
launchctl print system/org.nixos.darwin-store
# → Could not find service "org.nixos.darwin-store" in domain for system
security find-generic-password -s '<volume-uuid>' /Library/Keychains/System.keychain
# → entry present, "Encrypted volume password"The volume was intact. The keychain password was intact. The plists were intact. /etc/synthetic.conf, /etc/fstab, and the Nix block in /etc/zshrc were all intact. The only thing wrong: launchd had forgotten both services. Their plists existed on disk but they weren't registered with the system domain.
The fix for that is supposed to be simple:
sudo launchctl bootstrap system /Library/LaunchDaemons/org.nixos.darwin-store.plist
sudo launchctl bootstrap system /Library/LaunchDaemons/org.nixos.nix-daemon.plistThis worked. /nix mounted automatically, the daemon started, nix --version produced a version number.
Then I rebooted, and everything was broken again.
The persistence trap
launchctl bootstrap loads a service into the current session. It does not — and this is the subtle part — persist that registration across reboots in the way I expected. At boot, launchd walks /Library/LaunchDaemons/ and loads what it finds there, but since Sonoma 14.6.1 that boot-time walk is gated by a separate system called Background Task Management (BTM).
BTM used to apply only to LaunchAgents and helper apps. As of 14.6.1 it also gates LaunchDaemons under /Library/LaunchDaemons/ if their executable doesn't carry an Apple Developer signature. The bootstrap command bypasses BTM for the running session. The BTM database itself is untouched. On the next boot, BTM wins.
Diagnosing with sfltool
The command that made this visible is sfltool dumpbtm. It prints the contents of the BTM store — every background item macOS knows about, with its disposition. Piped through grep:
sudo sfltool dumpbtm | grep -B2 -A8 -iE "nixos|darwin-store"Output:
Flags: [ legacy ] (0x1)
Disposition: [enabled, disallowed, notified] (0x9)
Identifier: 16.org.nixos.nix-daemon
URL: file:///Library/LaunchDaemons/org.nixos.nix-daemon.plist
Executable Path: /bin/sh
Parent Identifier: Unknown Developer
And an identical block for org.nixos.darwin-store.
Two things jump out:
disallowed. The services are registered in BTM, but BTM is actively preventing them from loading at boot. This is the exact mechanism that makes manuallaunchctl bootstrapfixes evaporate across reboots.Executable Path: /bin/shandParent Identifier: Unknown Developer. The plists invoke/bin/sh -c "..."rather than calling a signed binary directly. BTM identifies the service by its entry point — it sees/bin/sh, it sees no developer signature, and it treats it as an untrusted generic shell script.
This is also why the corresponding entries in System Settings don't say "Nix" anywhere. They appear as two "sh" items labeled "Item from unidentified developer."
The actual fix
- Open System Settings → General → Login Items & Extensions.
- Scroll to "Allow in the Background".
- Find two entries named sh with subtitle "Item from unidentified developer". (There may be one or two adjacent
shentries and an unrelatedshutdown-gpg-agent— ignore that one.) - Toggle both to ON.
No launchctl ceremony required. The toggles write directly to the BTM store, and the next reboot respects the new disposition.
If the toggles appear to already be ON but BTM still reports disallowed — which can happen when UI state and BTM state drift apart after an OS upgrade — flip them off and back on to force a write.
What I'd tell past-me
- When the nix-installer fails on a "reinstall", stop. It's not a repair tool. Its error messages assume you want a fresh install and the suggestions reflect that. Read them as "here's what I would do to start over", not "here's how to fix your system."
- Diagnosis before destruction.
diskutil apfs list,ls /nix,launchctl print,security find-generic-password— all read-only, all revealing. Five minutes of probing told me the volume was fine, the keychain was fine, and only launchd registration was missing. launchctl bootstrapis a session fix, not a persistence fix. If a bootstrap works but doesn't survive reboot, BTM is almost certainly involved. Skip the "try again with more sudo" phase and go straight tosfltool dumpbtm.sfltool dumpbtmis the diagnostic tool for this entire category of problems. It's undocumented-ish, rarely mentioned, and exactly what you need when a LaunchDaemon that should autostart doesn't.- Apple's trust perimeter keeps expanding. What used to be a mostly open area (system LaunchDaemons in
/Library/LaunchDaemons/) is now gated by BTM. Any third-party tool that relies on a script-wrapped daemon — Nix, Homebrew services, custom launch scripts, gpg-agent shutdown hooks — is a candidate for this same breakage after a major macOS update.
The depressing part is that this will happen again. Every macOS major release tightens another security boundary, and the symptom always looks like "the tool is broken" before it reveals itself as "the OS decided not to trust the tool anymore". Keeping a short incident log of what broke, what the diagnosis was, and what the fix looked like turns each round into a faster recovery than the last.