NixOS / nix-darwin / WSL
Nix-Based System Configuration

One Flake.
Many Machines.

Wayland experience straight outta r/unixporn. Encrypted secrets that even I can't accidentally leak. AI agents.
MacOS <--VMWare--> Wayland clipboard.

mitchell's config fork went overboard.

Installation

curl | sh

Apple Silicon & Intel

macOS

Installs Xcode CLT, Homebrew, Determinate Nix, clones the repo, and configures your entire Mac before you finish your coffee. Touch ID sudo.

$ curl -sL smallstepman.github.io/macbook.sh | sh

Then niks to rebuild, or vm bootstrap to summon the Linux box.

VMware Fusion / aarch64

NixOS VM

Pulls NixOS ISO, installs VMWare Fusion, creates a NixOS VM inside VMware Fusion, provisions it over shared folders.

$ curl -sL smallstepman.github.io/vm.sh | sh

Or, from a configured macOS host: vm bootstrap

Windows Subsystem for Linux

WSL

Enables WSL, installs a NixOS distribution, applies the flake config. All from an elevated PowerShell prompt.

PS> iex (iwr -useb smallstepman.github.io/wsl.ps1)

Headless NixOS with flakes. No GUI.

NixOS desktop with Niri tiling window manager, Doom Emacs, and Ghostty terminal

Niri · Noctalia Shell · Doom Emacs · Ghostty · NixOS 25.11

Desktop Environment

Wayland-Native
Tiling Desktop

Niri scrollable-tiling compositor with Noctalia shell — auto-themed from your wallpaper. greetd login manager, Ghostty for the main screen, foot for the quick throwaway.

Screenshot 2
Screenshot 3
Capabilities
Compositors

Niri + Noctalia

X11 lost. Niri tiles windows in an infinite scrollable strip (like a toilet paper). Noctalia themes everything from your wallpaper. Edge-navigation passthrough for all editors, terminals, and web browsers.

GIF — Niri tiling & theme switching
Editors & Languages

Dev Toolchain

Nix-managed Doom Emacs as a systemd daemon & Neovim w/ LazyVim (+VSCode mimicing doom emacs, just in case). Rust, Go, Python3 + uv, Node + fnm, and DevOps stack. Starship prompt.

GIF — Doom Emacs + Niri edge-nav passthrough
LLM-Native

AI Agents

Claude Code, OpenCode, and GitHub Copilot from llm-agents.nix.

sops-nix + Bitwarden

Encrypted Secrets

sops-nix + sopsidy keep boot-time secrets in an external generated dataset outside git, shared into the VM at /nixos-generated. Runtime tokens stay in Bitwarden/rbw and are injected per-process.

Cross-Platform

Shared Clipboard

VMware Fusion doesn't do Wayland clipboard. Patched uniclip so copy-paste work across operating systems now.

GIF — Clipboard sharing macOS ↔ VM
macOS Integration

Homebrew + Launchd

Some things Nix can't manage. Discord, Spotify, LM Studio, Mullvad VPN. Colemak layout (with right hand shifted 2 cols to the right so it's comfy) via Kanata keyboard remapping with homerowmod. sudo using TouchID.

VMware Fusion

VM Orchestration

Full VM lifecycle from your Mac: bootstrap from zero, rebuild over SSH, start, stop, grab IP. DHCP pinned at 192.168.130.3. Shared folders. Docker context forwarding from host MacOS to VM to save the battery.

GIF — vm bootstrap (terminal recording)
Networking

Tailscale + Tunnels

Tailscale VPN & SSH tunnels for Open WebUI and ActivityWatch between host and guest. VMware NAT with static DHCP reservation.

Disk & Boot

Declarative Everything

Disko for declarative disk layout, systemd-boot, greetd for login. GPG-signed commits require TouchID. git-repo-manager to pull wallpapers mostly.

Architecture Built on den

Aspect-Oriented Composition

Instead of one massive file per machine — the NixOS starter pack — every concern is a reusable aspect: a self-contained slice of configuration that composes nixos, darwin, and homeManager options in one place. Hosts just pick which aspects they need. It sounds obvious when you say it out loud. It took embarrassingly long to get right.

How the flake assembles

flake.nix │ ├── imports ./den/default.nix # den bootstrap, overlays, host modules ├── imports ./den/hosts.nix # host declarations + metadata └── inherit (den.flake) # nixosConfigurations, darwinConfigurations den/hosts.nix ├── den.hosts.aarch64-linux.vm-aarch64 # NixOS VM ├── den.hosts.aarch64-darwin.macbook-pro-m1 # macOS └── den.hosts.x86_64-linux.wsl # Windows WSL den/aspects/ ├── hosts/ │ ├── vm-aarch64.nix # → linux-core, secrets, linux-desktop, vmware │ ├── macbook-pro-m1.nix # → darwin-core, darwin-desktop, homebrew, launchd │ └── wsl.nix # → wsl-system ├── features/ │ ├── ai-tools.nix # Claude Code, OpenCode, llm-agents │ ├── darwin-core.nix # nix.enable=false, Touch ID sudo, system state │ ├── darwin-desktop.nix # yabai, skhd, Kanata, macOS UX │ ├── editors-devtools.nix # Doom Emacs, Neovim, tmux, Rust, Go, Python │ ├── gpg.nix # GPG agent, signing, pinentry │ ├── home-base.nix # XDG dirs, fonts, base packages │ ├── homebrew.nix # casks, brews, mas apps │ ├── identity.nix # user account, SSH keys, git identity │ ├── launchd.nix # uniclip server, Open WebUI, ActivityWatch │ ├── linux-core.nix # NixOS baseline, networking, systemd │ ├── linux-desktop.nix # Niri, Noctalia, greetd, Ghostty │ ├── secrets.nix # sops-nix, sopsidy, rbw │ ├── shell-git.nix # zsh, direnv, atuin, git config │ ├── vmware.nix # VMware guest, uniclip, Docker forwarding │ └── wsl.nix # WSL integration, default user, no GUI └── users/ └── m.nix # user composition: identity → shell → editors → ai
Security Model

Zero Trust

Secret values never touch version control — the repo keeps only declarative wiring, while boot-time secrets live in an external generated dataset outside git and the VM keeps the matching age private key. Runtime tokens stay in a EU-based Bitwarden vault and are injected only into the process that needs them.
┌─ Build Time ──────────────────────────────────────────────────────────────┐ │ │ │ nix run path:$WRAPPER#collect-secrets # macOS, via wrapper flake │ │ │ │ │ ├── reads VM age pubkey # ~/.local/share/nix-config-generated │ │ ├── fetches boot secrets # rbw / EU-hosted Bitwarden │ │ └── writes secrets.yaml # external dataset, outside git │ │ │ ├─ Deploy Time ─────────────────────────────────────────────────────────────┤ │ │ │ sops-nix on the VM # decrypts generated input at activation │ │ ├── Tailscale auth key # /run/secrets/tailscale/auth-key │ │ ├── rbw email + Uniclip # rbw/email + uniclip/password │ │ └── User password hash # /run/secrets/user/hashed-password │ │ │ ├─ Runtime ─────────────────────────────────────────────────────────────────┤ │ │ │ API keys injected per-process # NEVER as global env vars │ │ ├── gh() / claude() / codex() # fetch via rbw on demand │ │ └── with-openai / with-amp # one process, one token scope │ │ │ └───────────────────────────────────────────────────────────────────────────┘

Nothing useful in git

No plaintext secrets, no tracked generated dataset, no .env.example, no "just set these env vars" README. The repo keeps only declarative secret wiring; ~/.local/share/nix-config-generated and the VM age private key live outside git. You could publish the whole repo and it wouldn't matter. (It is published. Hi.)

Per-process only

API keys are fetched from Bitwarden the moment they're needed and scoped to exactly one process. Your shell session never meets OPENAI_API_KEY.

Machine-locked keys

Each host has its own age keypair. The VM can't decrypt macOS secrets. The Mac can't decrypt VM secrets. A compromised machine gets exactly the secrets it was supposed to have and nothing else.

Biometrics & signed commits

sudo and GPG-commit-sign via Touch ID. Every commit signed with GPG key 247AE5FC6A838272.

Cross-Platform

Transparent Clipboard

VMware Fusion doesn't support Wayland clipboard sharing. Patched uniclip Go source to add --bind and UNICLIP_PASSWORD support, compiled it from source through Nix, and deployed it as managed services on both sides. Copy on macOS, paste in the VM, and vice-versa. Encrypted. Automatic. No manual setup after bootstrap.

macOS (host) NixOS VM (guest) ┌────────────────────────────────┐ ┌────────────────────────────────┐launchd user agent │ │ systemd user service │ │ │ │ │ │ uniclip server --secure │ │ uniclip client --secure │ │ --bind 192.168.130.1 │ TCP/TLS │ 192.168.130.1:53701 │ │ -p 53701◄──────────► │ │ │ │ │ │ │ UNICLIP_PASSWORD from rbw │ │ UNICLIP_PASSWORD from sops└────────────────────────────────┘ └────────────────────────────────┘
Workflow

The Incantations

Everything is a short alias because I'll forget anything longer than four characters. niks rebuilds whatever system you're on. vm manages the Linux VM from macOS. That's the whole interface.

VM Lifecycle

CommandDescription
vm bootstrap [--redo]Everything from scratch. --redo destroys what's there first. No confirmation. Be sure.
vm switchApply config changes (nixos-rebuild switch over SSH)
vm upStart the VM
vm downGraceful shutdown
vm ssh [cmd]SSH into the VM, or run a remote command
vm ipPrint the VM's current IP address
vm refresh-secretsRefresh the external generated dataset (age pubkey, SSH pubkeys, encrypted secrets)

System Rebuild

AliasDescription
niksRebuild & switch — figures out if you're on macOS or NixOS and does the right thing
niktBuild without switching. For the cautious. (Or the recently burned.)
Platform Matrix

What Each Machine Gets

ComponentNixOS VMmacOSWSL
Window ManagerNiri / Mango (Wayland)yabai + skhd
TerminalGhostty, footNative TerminalWindows Terminal
PackagesNix onlyNix + HomebrewNix only
EditorsDoom Emacs (daemon), NeovimDoom Emacs, NeovimNeovim
GUI AppsLibreWolf, ChromiumVia Homebrew casks
DockerVia SSH to macOSOrbStackDocker Desktop
ClipboardUniclip clientUniclip server
AI Agents20+ via llm-agentsClaude, Copilot20+ via llm-agents
Quality

11 Test Suites

Every aspect validated independently because I've been burned by "it works on my machine" and my machine is three machines. Flake eval, host schemas, Home Manager integration, platform-specific config — all checked before any switch.

# tests/ ├── flake-smoke.sh # Flake evaluation sanity check ├── host-schema.sh # Host declaration schema validation ├── home-manager-core.sh # Home Manager module integration ├── identity.sh # User identity and shell provisioning ├── devtools.sh # Editors, languages, dev tools present ├── darwin.sh # macOS-specific config (nix-darwin) ├── linux-core.sh # NixOS system baseline ├── vm-desktop.sh # Graphical desktop (Niri, Noctalia, greetd) ├── wsl.sh # WSL integration ├── no-legacy.sh # Ensures no legacy code patterns remain └── gpg-preset-passphrase.sh # GPG signing verification
Flake Inputs

The Dependencies

Stable channel for sanity, unstable for impatience. Everything pinned in flake.lock so "it worked yesterday" is a verifiable claim.

Core

nixpkgs 25.11

Framework

den

macOS

nix-darwin

Users

home-manager

Compositor

niri + noctalia

Editors

doom-emacs + lazyvim-nix

Secrets

sops-nix + sopsidy

Rust

rust-overlay

AI

llm-agents.nix

Disk

disko

WSL

nixos-wsl

Window Control

mangowc