A Nix flake that shares one module tree across NixOS, macOS and home-manager
Declarative system configurations using nixOS, nix-darwin, and home-manager
At a glance
- What is it?
- This personal system configuration manages the author's macOS, NixOS and Linux machines from a single flake built on flake-parts and a dendritic module pattern, with the home-manager configuration fully decoupled so the same tree can be applied to a plain Linux or macOS machine. It is MIT licensed, it is one person's dotfiles rather than a framework, and its SSH key and sops secrets workflow is the part that is genuinely hard to copy.
- Who is it for?
- Adopt this repository as a reference if you already live in Nix and want to see how one person wires NixOS, nix-darwin and home-manager together without duplicating modules, since the dendritic pattern and the decoupled home tree are the parts worth studying. Do not adopt it as a starting point if you are new to Nix, because the module system assumes you already know what an option, an overlay and a flake output are.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Nix, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One flake, three host classes and a module tree
This repository manages system configurations for all of the author's macOS, NixOS and Linux machines, and the design is worth reading for the structure rather than for the content, because the content is one person's preferences.
The flake follows the dendritic pattern on top of flake-parts. The flake.nix file holds the inputs, the concrete host outputs, which are nixosConfigurations, darwinConfigurations and homeConfigurations, and the perSystem wiring for overlays, devShell, treefmt, pre-commit and checks.
The modules are pulled in recursively by vic/import-tree, which pulls in every .nix file under the modules directory, and each one self-registers as a reusable module body under flake.<class>Modules.<name>. Hosts then compose those modules by name.
The tree itself is laid out like this:
modules/
├── _lib.nix # mkAspect helper (underscore-prefixed, skipped by import-tree)
├── shared/ # cross-class option modules + wiring
│ (primary-user, common-base, nixpkgs-wiring, identity, fonts)
├── nixos/ # flake.nixosModules.<name>
├── darwin/ # flake.darwinModules.<name>
├── home/ # flake.homeModules.<name> (includes assets/ for non-Nix files)
└── profiles/ # identity profiles registered across all three classesThe prefix on _lib.nix is not decoration, it is the mechanism, since import-tree skips files whose name starts with a prefix character and that is how the helper stays out of the module registry.
The repository also holds secrets, a pkgs directory for custom packages, a terraform directory, a .sops.yaml, a flake.lock, a .stylua.toml for formatting Nix code, and an AGENTS.md and CLAUDE.md.
Each module file registers itself and contains the whole body
The convention that makes the tree work is that a module file inlines its own body rather than importing a separate implementation file. The documentation gives the example: a file like modules/nixos/hyprland.nix both registers flake.nixosModules.hyprland and contains the full compositor configuration.
The consequence is that a module is one file you can read end to end, and there is no indirection between the name in a host's imports list and the settings it applies.
Cross-module references go through config.flake.<class>Modules.<name>, so the example given is that hm.imports = [ config.flake.homeModules.hyprland ] in the NixOS module is how the home-manager side of Hyprland is pulled in when Hyprland is enabled.
That last part is the interesting one. The NixOS module says nothing about home-manager directly; it references a home module through the shared registry, and the home module is registered under the home class. The two sides of a piece of software therefore live in their own trees and are joined at the point of use.
The non-Nix files that are not flake-parts modules live in secrets, in pkgs for custom packages, and in modules/home/assets for the dotfiles, nvim and yazi directories, which are referenced by source path from the corresponding home modules. So an editor configuration is a file in the tree and a module that points at it, rather than a string of file contents inside a Nix expression.
NixOS and nix-darwin share their defaults, and home-manager shares nothing
The two operating system classes overlap deliberately. nix-darwin and NixOS share identical shell, user, fonts and packages setup via modules/nixos/default.nix and modules/darwin/default.nix, with the shared option declarations named as user and hm, and the nixpkgs wiring factored out into the modules/shared directory.
The shared directory holds five modules, listed in the tree comment: primary-user, common-base, nixpkgs-wiring, identity and fonts. The point of factoring them out is that a change to the package set or the font configuration lands on both operating systems at once, which is the behaviour you want when the two are meant to look the same.
The home-manager configuration takes the opposite approach and is entirely decoupled from NixOS and nix-darwin. All of its modules live in modules/home, and for each NixOS or darwin host they are pulled in through the flake-parts hm alias, which forwards config.hm.* to home-manager.users.${config.user.name}.*.
The same module tree is also exposed as homeConfigurations in flake.nix, fanned out across x86_64-linux, aarch64-linux and aarch64-darwin, so it is usable as a standalone configuration on any Linux or macOS machine through the home-manager CLI. That is the part of the design that makes the repository more than one person's dotfiles: the same home configuration applies to a machine running macOS, a machine running NixOS and a machine running plain Linux, and the operating system specific parts are the only difference.
The forwarding through the user name is worth noting, because it means the home configuration is bound to one user on a NixOS host, and the shared primary-user module is what supplies that name.
Profiles, and why a profile spans three module classes
The user profile concept handles settings that depend on who you are rather than what you run.
Profiles live in modules/profiles, and the modules configure contextual, identity-specific settings such as SSL certificates or email addresses. The mechanism that makes them work across the three classes is the mkAspect helper from the _lib.nix module, and each profile is declared across all three module classes in a single file. The documentation notes that currently only personal.nix exists.
That is a small idea with real reach. A certificate or an email address is not a NixOS setting or a darwin setting or a home-manager setting in the abstract, it is a thing about you that needs to appear in all three places. Writing it once and having the aspect expand it into the right class avoids the usual three copies drifting apart.
The other things the identity profile carries are the credentials. The login password lives in secrets/users.yaml, and the repository sets mutableUsers to false, so there is no fallback password. That single setting is the reason the SSH host key step in the install procedure is not optional, and it is worth understanding before borrowing any of this.
Bootstrapping a machine, and the part that bites
The install instructions cover three operating system paths, and one of them is where the difficulty lives.
For a non-NixOS machine the first step is the Nix package manager itself, installed with a single command:
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install --determinateThat step is skipped on NixOS, where nix is the package manager by default. For Darwin and Linux the repository is cloned into ~/.nixpkgs, and a new system is bootstrapped with the nh helper, which auto-detects the host and installs nix-darwin or home-manager:
git clone https://github.com/kclejeune/system ~/.nixpkgsnix run .#nh -- darwin switch .#kclejeune@aarch64-darwinThe auto-detection can be overridden with --darwin or --home-manager if you need it.
NixOS hosts are provisioned remotely with nixos-anywhere, which partitions the disk from the host's disko config, builds the system locally and copies it over SSH. And that procedure has a dependency that the documentation is unusually explicit about: the host's SSH host key has to be pre-generated, because sops-nix decrypts secrets with it during nixos-install, and the login password lives in secrets/users.yaml with mutableUsers set to false, so there is no fallback. Without the key in place at install time the user ends up with no password.
The recovery path, and what a FIDO2 key changes
There is a documented recovery procedure, and reading it tells you how the failure mode is understood.
If a host installed without its secrets, for example with no login password, you finish the first two steps, boot the installer again, and rerun the install step with --disko-mode mount and --phases disko,install,reboot. That opens and mounts the existing disk instead of reformatting it. The documentation is honest that the log still prints Formatting hard drive with disko, and that it only asks for the passphrase once, so a person watching the output has to know that the disk is not being destroyed.
After the first boot there is one more step, and it is a security improvement rather than a convenience. You enrol a FIDO2 key for the LUKS passphrase:
sudo systemd-cryptenroll --fido2-device=auto /dev/disk/by-partlabel/disk-main-luksSo the sequence is: the SSH host key exists before the install so that the password secret can be decrypted, and once the machine is up a hardware key is enrolled so that the disk passphrase is not a human-typed secret on every boot.
There is one more detail about the installer itself. The minimal ISO has no NetworkManager, so Wi-Fi is joined with systemctl start wpa_supplicant and wpa_cli, a password is set with passwd, and the IP is noted so that the provisioning machine can copy an authorised key across with ssh-copy-id. And nixos-anywhere copies those authorised keys to root and reconnects as root, which is why key authentication is required and a password alone is not enough.
Editorial conclusion
Adopt this repository as a reference if you already live in Nix and want to see how one person wires NixOS, nix-darwin and home-manager together without duplicating modules, since the dendritic pattern and the decoupled home tree are the parts worth studying. Do not adopt it as a starting point if you are new to Nix, because the module system assumes you already know what an option, an overlay and a flake output are. Before you copy anything, check three things: that you understand the SSH host key and sops enrolment sequence, because a host installed without it ends up with no login password, that the module tree assumes a single primary user, and that the darwin and nixpkgs wiring is shared, so changing it affects every host at once.
Frequently asked questions
How does the kclejeune/system flake share modules between NixOS and macOS?
Through a dendritic module pattern on top of flake-parts, where vic/import-tree pulls in every .nix file under modules and each one self-registers as flake.<class>Modules.<name>. The shared setup lives in modules/shared, with the nixos and darwin default modules holding identical shell, user, fonts and packages configuration.
How is the home-manager configuration kept separate from the system configuration?
All home modules live in modules/home and are pulled into each host through the flake-parts hm alias, which forwards config.hm.* to home-manager.users.${config.user.name}.*. The same tree is also exposed as homeConfigurations for x86_64-linux, aarch64-linux and aarch64-darwin, so it works standalone through the home-manager CLI.
What has to happen before installing a NixOS host?
The host's SSH host key has to be pre-generated, because sops-nix decrypts secrets with it during nixos-install and the login password in secrets/users.yaml has no fallback with mutableUsers set to false. The key is then enrolled in sops and the file re-encrypted before the installer ISO is booted.
How do I bootstrap a macOS or Linux machine from this configuration?
Install the Nix package manager with the Determinate Systems installer unless you are on NixOS, clone the repository into ~/.nixpkgs, and run nix run .#nh -- darwin switch .#kclejeune@aarch64-darwin. The nh helper auto-detects the host and installs nix-darwin or home-manager, with --darwin and --home-manager as overrides.
What happens if a NixOS host is installed without its secrets?
The documented recovery is to finish the key generation and sops enrolment steps, boot the installer again, and rerun the install with --disko-mode mount and --phases disko,install,reboot, which unlocks and mounts the existing disk instead of reformatting it. The log still prints that the hard drive is being formatted, so the operator needs to know that.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/kclejeune-system)Was this useful?