If you've been using a Mac for development, you're probably familiar with the ritual: install Homebrew, brew install your tools, scatter dotfiles across your home directory, and hope you remember what you did when it's time to set up a new machine.
I recently migrated my entire macOS setup to Nix, and while the learning curve was real, the payoff is worth it. This guide walks through why you'd want to do this and how to actually pull it off.
Why Bother?
The pitch for Nix is simple: declarative, reproducible system configuration. Instead of running commands and hoping you remember them later, you define your entire setup in version-controlled config files. New machine? Clone your repo, run one command, done.
Here's what sold me:
- Single source of truth: One directory contains your entire system config
- Reproducible: The same config produces the same system, every time
- Atomic updates: Changes are transactional—if something breaks, roll back instantly
- Version controlled: Git tracks every change to your setup
- Scalable for Agencies: For teams, this solves onboarding. We can spin up new machines for coworkers with identical, secure environments instantly. It eliminates the "it works on my machine" friction because everyone is essentially running the exact same machine.
The AI Advantage
There is a modern bonus to this approach: Nix is incredibly AI-friendly. Because your system state is defined explicitly in code, AI tools (like Claude Code or Cursor) can easily read, understand, and maintain your environment. Instead of an agent having to guess if a tool is hidden in global npm, a Homebrew cellar, a DMG manual install, or the Mac App Store, it simply looks at your config file. It knows exactly where everything is, making automated updates and dependency management reliable rather than a guessing game.
The tradeoff is complexity. Nix has its own language, its own package manager, and concepts that take time to internalize. But if you've ever spent an afternoon trying to recreate your setup on a new laptop, that complexity starts looking like a fair trade.
The Stack
We'll use three tools:
- nix-darwin: System-level macOS configuration (packages, system preferences, services)
- home-manager: User-level config (dotfiles, shell, programs)
- Flakes: The modern way to pin dependencies and make builds reproducible
The key insight: we're not going all-or-nothing. Homebrew stays for GUI apps (managing casks declaratively through nix-darwin), while CLI tools move to Nix.
Getting Started
First, install Nix. The Determinate installer is the smoothest option:
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- installCreate your config directory:
mkdir -p ~/.config/nix/{hosts/mbp,home,dotfiles}
cd ~/.config/nix
git initThe Flake
The flake.nix is your entry point. It declares inputs (where to get nix-darwin, home-manager, and nixpkgs) and outputs (your system configuration):
{
description = "My nix-darwin configuration";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
nix-darwin = {
url = "github:LnL7/nix-darwin/master";
inputs.nixpkgs.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs@{ self, nixpkgs, nix-darwin, home-manager }:
{
darwinConfigurations."mbp" = nix-darwin.lib.darwinSystem {
system = "aarch64-darwin";
modules = [
./hosts/mbp/configuration.nix
home-manager.darwinModules.home-manager
{
users.users.yourname = {
name = "yourname";
home = "/Users/yourname";
};
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.backupFileExtension = "backup";
home-manager.users.yourname = import ./home/home.nix;
}
];
};
};
}System Configuration
The hosts/mbp/configuration.nix handles system-level stuff—packages, Homebrew casks, and macOS preferences:
{ pkgs, ... }:
{
system.primaryUser = "yourname";
# CLI tools (replaces brew install)
environment.systemPackages = with pkgs; [
bat fzf delta gh lazygit lsd ripgrep tree
];
# GUI apps (declarative Homebrew)
homebrew = {
enable = true;
onActivation.cleanup = "zap";
casks = [ "raycast" "slack" "linear-linear" "ghostty" ];
};
# macOS preferences
system.defaults = {
dock.autohide = true;
finder.AppleShowAllFiles = true;
NSGlobalDomain.KeyRepeat = 2;
};
# Determinate installer manages Nix itself
nix.enable = false;
system.stateVersion = 5;
nixpkgs.hostPlatform = "aarch64-darwin";
}The homebrew.onActivation.cleanup = "zap" is aggressive—it removes any cask not in your list. Great for keeping things tidy, but be aware of what you're opting into.
Home Manager
The home/home.nix handles user-level config—your shell, git, and dotfiles:
{ config, pkgs, lib, ... }:
{
programs.zsh = {
enable = true;
shellAliases = {
cat = "bat --style=header,grid";
ls = "lsd";
lg = "lazygit";
};
initContent = ''
eval "$(fnm env --use-on-cd --shell zsh)"
# ... rest of your shell config
'';
};
programs.git = {
enable = true;
settings = {
user.name = "Your Name";
user.email = "you@example.com";
push.autoSetupRemote = true;
};
};
programs.starship.enable = true;
home.stateVersion = "24.11";
}Building and Activating
With your config files in place:
nix build .#darwinConfigurations.mbp.system
sudo ./result/sw/bin/darwin-rebuild switch --flake .#mbpThe first run takes a while—Nix downloads and builds everything. Subsequent rebuilds are fast since unchanged packages are cached.
If you hit errors about existing files, Nix is being cautious. You might need to:
sudo mv /etc/zshenv /etc/zshenv.before-nix-darwinDay-to-Day Usage
Installing a new CLI tool:
- Add it to
environment.systemPackagesin your config - Run
darwin-rebuild switch --flake ~/.config/nix#mbp
Installing a new GUI app:
- Add it to
homebrew.casks - Run the same rebuild command
To search for packages:
nix search nixpkgs ripgrepOr use search.nixos.org for a faster web interface.
The New Machine Test
The real payoff comes when you set up a new Mac:
# Install Nix
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
# Clone your config
git clone git@github.com:you/nix-config.git ~/.config/nix
# Build and activate
cd ~/.config/nix
nix build .#darwinConfigurations.mbp.system
sudo ./result/sw/bin/darwin-rebuild switch --flake .#mbpThat's it. Your entire development environment, installed and configured.
Gotchas
A few things I ran into:
- nix-darwin updates frequently: Options get renamed. When you see deprecation warnings, check the nix-darwin options search
- Secrets stay manual: SSH keys, GPG keys, API tokens—these don't go in your repo. Transfer them separately
- Shell needs reloading: After
darwin-rebuild switch, open a new terminal to see changes
Worth It?
For me, yes. There's something satisfying about git diff-ing your system configuration.
This isn't the only way to manage a Mac setup—tools like chezmoi or plain dotfile repos work fine for many people. But if reproducibility matters to you, Nix is worth the learning curve.
The full config from this guide is on GitHub. Go fork it and make it yours.

