Blog
Newest Dev Report

Clan + macOS

Clan can now manage macOS machines

Michael Hoang Core Developer
9 Sep 2025 · 3 min

Over the last year, we’ve been progressively adding macOS support to Clan. This began with sticking a M4 Mac mini in a basement to run all of our builds and test suite. In the beginning, we couldn’t manage this Mac using Clan so we created a repo separate to clan-infra and used nix-darwin directly to configure the machine.

After a while, we started to notice that the GUI and CLI would periodically stop working on macOS as breaking changes would accumulate as the majority of the team runs Linux. So we decided that it was time to actually put the Mac mini to work by configuring it to build the GUI, CLI and all the tests on every PR to ensure they work on macOS as well.

At this point, the CLI ran and worked well on macOS allowing you to deploy to NixOS machines using the clan machines install and clan machines update commands.

However, we wanted macOS machines to be first-class Clan members, with software, settings, and secrets managed declaratively like Linux, so we began adding nix-darwin support to Clan.

What is nix-darwin

nix-darwin is a framework that uses the module system from NixOS to manage macOS, allowing you to bring the reproducibility and declarative power of Nix to macOS.

Clan automatically imports a core set of modules into all machines to provide features like remote deployment and vars. These modules were written specifically for NixOS, but are necessary to make Clan useful on macOS and other systems so we needed to extend them.

multi-OS modules

The NixOS module system has been made into a library and is now used in nix-darwin, Home Manager and other Nix-based projects. To keep a module from being loaded by the wrong module system, authors should set the _class attribute to nixos or darwin.

We could extend the core Clan modules to support macOS is to duplicate all the code for nix-darwin, however this would lead to a lot of extra maintainence burden. So instead we decided to make modules that support both NixOS and nix-darwin.

Previously, detecting which module system you were in required hacks like options ? virtualisation which works due to the virtualisation option tree existing only in NixOS and not nix-darwin:

{ options, config, lib, ... }:
{
  config = lib.optionalAttrs (options ? virtualisation) {
    # NixOS specific configuration
  };
}

This hack does not work inside of imports meaning you are not able to do conditional imports:

{ options, config, lib, ... }:
let
  nixosModule = { ... }: {
    # NixOS specific configuration
  };
  darwinModule = { ... }: {
    # nix-darwin specific configuration
  };
in {
  imports = [
    (lib.optionalAttrs (options ? virtualisation) nixosModule)
    (lib.optionalAttrs (options ? launchd) darwinModule)
  ];

  config = {
    # shared configuration
  };
}

This led to me making my first ever PR to improve the module system, which added _class to the module arguments allowing conditional imports based on the module system class:

{ _class, options, config, lib, ... }:
let
  nixosModule = { ... }: {
    # NixOS specific configuration
  };
  darwinModule = { ... }: {
    # nix-darwin specific configuration
  };
in {
  imports = [
    (lib.optionalAttrs (_class == "nixos") nixosModule)
    (lib.optionalAttrs (_class == "darwin") darwinModule)
  ];

  config = {
    # shared configuration
  };
}

This means you can now write modules that support multiple module systems without needing to rely on hacks.

Using this new feature, we have added vars and deployment support to nix-darwin machines managed by Clan.

If you’d like to try it out, you can check out our macOS guide.

Get started with our framework

With Clan you can create customized installation images, and skip time consuming manual installation steps