Mounting /nix automatically on MacOS

Anwer KhanAnwer Khan
5 min read

I use the Determinate Nix installer to provide nix on my MacMini desktop. It creates a partition for /nix, but does not mount it automatically on boot, because that creates the potential for some problems that are explained well by AI chatbots. This leaves it up to the user to mount and activate the nix system as needed, while also enhancing reliability and durability against system breakage during macOS updates.

But some of us have configuration that we want activated on boot, for things like shells, and apps installed via nix that we want available immediately. After seeking a good solution with the help of AI I settled on using a LaunchAgent to mount the partition. I keep it on an external drive, as disk space is expensive on Apple hardware.

Here is the LaunchAgent offered by Grok. The specifics of the disk partition are particular to one’s system and must be inserted in place of diskXsY in the code sample.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.nix.mount</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/sh</string>
        <string>-c</string>
        <string>/usr/sbin/diskutil mount -mountPoint /nix diskXsY</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <false/>
</dict>
</plist>

I realized that this configuration could be added to the declarative system definition used by nix-darwin, which helps with reproducibility. However, the volume identifier diskXsY might change in a new system or even with changes in storage configuration in one’s current system, and recording the UUID in place of diskXsY can be protective against breakage, while also clarifying the information needed when transfering this snippet to a new system. It is one line to find the UUID after installation of nix:

diskutil info /nix | grep "Volume UUID"

In keeping with my goals of modularity and reusability, I keep this bit of configuration in a host-specific module that is imported. The UUID shown in this snippet is for my system, and should be replaced with the results found from diskutil for others using this code.

# hosts/macminim4/nix-store-uuid.nix
{ config, lib, ... }:
{
  custom.nix-mount.uuid = "E1A6C722-457C-4C80-AA6C-098431B3BD0D";
}

I use a multi-host configuration repository, and created a module usable by all nix-darwin hosts to import and thus mount /nix.It doesn’t contain any host-specific details as the partition UUID is already separated in the module above.

# modules/nix-darwin/nix-mount.nix
{ config, lib, pkgs, ... }:
let
  cfg = config.custom.nix-mount;

  # Create an executable script in the Nix store
  fixNixMountPlistScript = pkgs.runCommand "fix-nix-mount-plist.sh" {
    src = ./fix-nix-mount-plist.sh;
  } ''
    mkdir -p $out
    cp $src $out/fix-nix-mount-plist.sh
    chmod +x $out/fix-nix-mount-plist.sh
  '';
in
{
  options.custom.nix-mount = {
    uuid = lib.mkOption {
      type = lib.types.str;
      description = "UUID of the /nix volume to mount";
      example = "12345678-1234-1234-1234-1234567890AB";
    };
  };

  config = {
    assertions = [
      {
        assertion = cfg.uuid != "";
        message = "custom.nix-mount.uuid must be set for nix-mount daemon";
      }
    ];

    launchd.daemons.nix-mount = {
      serviceConfig = {
        ProgramArguments = [
          "/bin/sh"
          "-c"
          # Initial 5-second delay, retry every 5 seconds for 60 seconds
          ''/bin/sleep 5; for i in {1..12}; do /usr/sbin/diskutil mount -mountPoint /nix ${cfg.uuid} && exit 0; /bin/sleep 5; done; exit 1''
        ];
        Label = "com.nix.mount";
        RunAtLoad = true;
        KeepAlive = false;
        StandardErrorPath = "/tmp/nix-mount-error.log";
        StandardOutPath = "/tmp/nix-mount-out.log";
      };
    };

    environment.etc."fix-nix-mount-plist.sh".source = "${fixNixMountPlistScript}/fix-nix-mount-plist.sh";

    system.activationScripts.fixLaunchDaemonPermissions.text = ''
      /bin/sh /run/current-system/sw/etc/fix-nix-mount-plist.sh
    '';
  };
}

There is code added for robustness, since my Nix Store is on a USB drive, which takes some time during system boot before partitions on it become available for mounting. Thus there is a retry loop that makes repeated attempts to mount, within a 60-second time limit. There is also logging code useful for debugging.

LaunchDaemons on macOS must have 644 permissions to work, and setting permissions on the plist file created by the system definition files was difficult for me. Standard nix methods for setting permissions, suggested by AI, did not work for reasons that were difficult to debug. The ultimate solution was to create an imperative script to change the permissions on the newly-created plist file, on system activation. It is a messy solution but it seems to work. I have placed the script in the same directory as the module that mounts /nix, but this isn’t necessary. Other locations can be used, with the code that loads it adjusted accordingly. The script that changes permissions must be executable. This is handled in the configuration file - as can be seen above - for easy reproducibility. One benefit of keeping the script in its own file, and not simply defining it within the module, is that it can be viewed or edited with syntax highlighting that matches the file format (shell script).

#!/bin/sh
LOG="/tmp/nix-darwin-activation.log"
echo "$(date): Running fix-nix-mount-plist.sh" >> "$LOG"
PLIST="/Library/LaunchDaemons/com.nix.mount.plist"
# Wait up to 5 seconds for plist to appear
for i in {1..5}; do
  if [ -f "$PLIST" ]; then
    echo "$(date): Found $PLIST, setting permissions" >> "$LOG"
    /bin/chmod 644 "$PLIST" 2>> "$LOG" || {
      echo "$(date): Failed to chmod 644 $PLIST" >> "$LOG"
      exit 1
    }
    /bin/chown root:wheel "$PLIST" 2>> "$LOG" || {
      echo "$(date): Failed to chown $PLIST" >> "$LOG"
      exit 1
    }
    echo "$(date): Successfully set permissions on $PLIST" >> "$LOG"
    exit 0
  fi
  echo "$(date): Waiting for $PLIST ($i/5)" >> "$LOG"
  sleep 1
done
echo "$(date): Error: $PLIST not found after 5 seconds" >> "$LOG"
exit 1

This use of a synthetic mount for /nix requires that the contents of /etc/synthetic.conf include a specification for this partition. The Determinate Nix installer should have taken care of this already, and it’s difficult to manage the file using nix-darwin itself. But it’s worth checking for the presence of nix in the file, for the sake of troubleshooting if problems arise. Here are the contents in my system:

run     private/var/run
nix

After a successful darwin-rebuild one can verify that the LaunchAgent has been created in the right place, and contents of the file can be examined with a command like cat

ls -l /Library/LaunchDaemons/com.nix.mount.plist

.r--r--r-- root wheel 719 B Fri May 23 13:40:18 2025 /Library/LaunchDaemons/com.nix.mount.plist

0
Subscribe to my newsletter

Read articles from Anwer Khan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Anwer Khan
Anwer Khan

Data and ML engineer, interested in payments technology