Everything You Can Set on macOS with nix-darwin
6 min readMost people discover Nix on macOS through packages. nix-env -iA nixpkgs.ripgrep, a few shell tools, maybe a dev environment. Useful, but it leaves the rest of your system untouched — a sprawl of defaults write commands, System Settings clicks you can’t remember, and a Dock that resets itself after every migration.
nix-darwin changes the scope. It’s a NixOS-style module system for macOS: you describe your system in Nix, run darwin-rebuild switch, and it converges. Packages, preferences, services, keyboard remapping, Homebrew casks, Dock layout — all from one configuration. Wipe the machine, rebuild, and everything is back exactly as it was.
This isn’t a setup tutorial. It’s a tour of what’s actually available — with real config snippets you can drop into your own darwin-configuration.nix and adapt.
System defaults — the big one
The system.defaults attrset is where nix-darwin really flexes. Every defaults write you’ve ever cargo-culted from a dotfiles repo has a typed, declarative equivalent here.
NSGlobalDomain covers the settings that apply everywhere — dark mode, key repeat, scroll direction, that infuriating beep:
system.defaults.NSGlobalDomain = {
AppleInterfaceStyle = "Dark";
ApplePressAndHoldEnabled = false;
KeyRepeat = 2;
InitialKeyRepeat = 15;
"com.apple.swipescrolldirection" = false;
"com.apple.sound.beep.volume" = 0.0;
"com.apple.sound.beep.feedback" = 0;
};
ApplePressAndHoldEnabled = false is the one that gives you key repeat instead of the accent character picker. If you’ve ever held down j in Vim and watched nothing happen, this is why.
Dock — hide it, size it, kill recents, disable space rearranging, set hot corners:
system.defaults.dock = {
autohide = false;
tilesize = 48;
show-recents = false;
mru-spaces = false;
minimize-to-application = true;
expose-animation-duration = 0.2;
# Hot corners: 4=Desktop, 5=Screensaver, 12=Notification Center, 14=Quick Note
wvous-bl-corner = 4;
wvous-br-corner = 14;
wvous-tl-corner = 5;
wvous-tr-corner = 12;
};
mru-spaces = false stops macOS from silently reordering your spaces based on usage. If you use numbered spaces and expect Space 3 to stay in position 3, you need this.
Finder — show hidden files, default to list view, add a quit menu item:
system.defaults.finder = {
AppleShowAllExtensions = true;
AppleShowAllFiles = true;
FXEnableExtensionChangeWarning = false;
FXPreferredViewStyle = "Nlsv"; # list view
FXRemoveOldTrashItems = true;
QuitMenuItem = true;
ShowPathbar = true;
ShowStatusBar = true;
};
QuitMenuItem = true adds Cmd+Q to Finder. Sounds minor until you realize there’s no other way to fully quit it without killall Finder.
Trackpad — tap to click and three-finger drag:
system.defaults.trackpad = {
Clicking = true;
TrackpadRightClick = true;
TrackpadThreeFingerDrag = true;
};
Screen capture — change the save location, format, and kill that thumbnail preview:
system.defaults.screencapture = {
location = "~/Downloads";
type = "png";
disable-shadow = true;
show-thumbnail = false;
};
Menu bar clock:
system.defaults.menuExtraClock = {
Show24Hour = true;
ShowSeconds = false;
ShowDate = 0;
};
Login window:
system.defaults.loginwindow = {
GuestEnabled = false;
DisableConsoleAccess = true;
};
Spaces across displays:
system.defaults.spaces.spans-displays = true;
That’s a lot of defaults write commands you no longer need to remember.
CustomUserPreferences — the escape hatch
Not everything has a typed option in nix-darwin. CustomUserPreferences is a freeform attrset that maps directly to defaults write — any domain, any key. Think of it as the declarative version of your post-install shell script:
system.defaults.CustomUserPreferences = {
"com.apple.finder" = {
ShowExternalHardDrivesOnDesktop = false;
ShowRemovableMediaOnDesktop = false;
_FXSortFoldersFirst = true;
FXDefaultSearchScope = "SCcf"; # search current folder
};
"com.apple.desktopservices" = {
DSDontWriteNetworkStores = true;
DSDontWriteUSBStores = true;
};
"com.apple.screensaver" = {
askForPassword = 1;
askForPasswordDelay = 0;
};
"com.apple.AdLib".allowApplePersonalizedAdvertising = false;
"com.apple.SoftwareUpdate" = {
AutomaticCheckEnabled = true;
ScheduleFrequency = 1;
AutomaticDownload = 1;
CriticalUpdateInstall = 1;
};
"com.apple.TimeMachine".DoNotOfferNewDisksForBackup = true;
"com.apple.WindowManager" = {
HideDesktop = true;
StandardHideDesktopIcons = true;
};
};
DSDontWriteNetworkStores and DSDontWriteUSBStores stop macOS from scattering .DS_Store files across every network share and USB drive you mount. If you’ve ever committed a .DS_Store to a repo, you know why this matters.
You can also disable keyboard shortcuts like Ctrl+Space (input source switching) through com.apple.symbolichotkeys if you need that binding for something else.
Keyboard remapping
Caps Lock as Control. Two lines, no Karabiner:
system.keyboard = {
enableKeyMapping = true;
remapCapsLockToControl = true;
};
This uses the system-level key remapping — it works everywhere, including the login screen.
Touch ID for sudo
One line. No more editing /etc/pam.d/sudo_local by hand and hoping a macOS update doesn’t overwrite it:
security.pam.services.sudo_local.touchIdAuth = true;
Every sudo prompt becomes a fingerprint tap. This is the single most satisfying line in my entire nix-darwin config.
Declarative Homebrew
Nix handles most packages, but some macOS GUI apps only exist as Homebrew casks, and some things only live on the Mac App Store. nix-darwin can manage Homebrew itself — casks, taps, MAS apps, and automatic cleanup of anything not declared:
homebrew = {
enable = true;
caskArgs.no_quarantine = true;
onActivation = {
autoUpdate = true;
cleanup = "uninstall"; # remove anything not declared
upgrade = true;
};
casks = [ "firefox" "notion" "slack" "discord" ];
masApps = {
"WhatsApp" = 310633997;
"Xcode" = 497799835;
};
};
cleanup = "uninstall" is the key line. Anything installed via Homebrew that isn’t in your casks or masApps list gets removed on rebuild. Your Mac only has what you’ve declared — nothing more.
Those numeric IDs come from the Mac App Store. Look them up with mas:
mas search WhatsApp
# 310633997 WhatsApp Messenger (24.9.80)
mas search Xcode
# 497799835 Xcode (16.3)
The number on the left is what goes in your masApps attrset. You can also grab it from any App Store URL — it’s the number after /id in https://apps.apple.com/app/whatsapp-messenger/id310633997.
Nix settings — binary caches and remote builders
Flakes, substituters, distributed builds to a Linux box:
nix.settings = {
experimental-features = [ "nix-command" "flakes" ];
trusted-users = [ "root" "@admin" "youruser" ];
substituters = [ "https://cache.garnix.io" ];
trusted-public-keys = [
"cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g="
];
};
nix.distributedBuilds = true;
nix.buildMachines = [{
hostName = "10.0.0.50";
systems = [ "x86_64-linux" "aarch64-linux" ];
protocol = "ssh-ng";
maxJobs = 64;
supportedFeatures = [ "nixos-test" "benchmark" "big-parallel" "kvm" ];
}];
That trusted-users line is worth a closer look. Nix builds run as the nixbld daemon user, not as you — so by default the builder can’t see your SSH keys or GitHub tokens. If your flake inputs point at private repos, the build will fail with authentication errors.
Adding yourself (or @admin) to trusted-users lets the daemon inherit your user-level access tokens. Pair it with nix.extraOptions to tell the daemon where to find your Git credentials:
nix.extraOptions = ''
!include /etc/nix/access-tokens.conf
'';
Then drop a file at /etc/nix/access-tokens.conf — outside the store, so it stays out of world-readable /nix/store — with:
access-tokens = github.com=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Now nix build and darwin-rebuild switch can pull private flake inputs without manual git clone. The token never lands in the Nix store and you can rotate it in one place.
Services
nix-darwin can manage launchd services the same way NixOS manages systemd units. OpenSSH is the most common:
services.openssh = {
enable = true;
extraConfig = ''
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
'';
};
But the service catalog goes well beyond SSH — yabai, skhd, sketchybar, lorri, and the nix-daemon itself are all managed this way.
Activation scripts
Need to run arbitrary shell commands on every darwin-rebuild switch? Activation scripts:
system.activationScripts.postActivation.text = ''
chsh -s /run/current-system/sw/bin/fish youruser
'';
This is the escape hatch for anything nix-darwin doesn’t have a module for. It runs as root during activation, so you can set ownership, create directories, modify system files — whatever the rebuild needs to converge.
Dock entries with dockutil
If you use a dockutil wrapper module, you can set exact Dock contents declaratively:
local.dock.entries = [
{ path = "/Applications/Safari.app"; }
{ path = "/Applications/Slack.app"; }
{ path = "/System/Applications/Music.app"; }
];
No more dragging icons around after a fresh install. The Dock shows exactly what you declared — nothing more, nothing less.
Everything else
There’s more than fits in one post. A quick survey of other nix-darwin options worth exploring:
fonts.packages— declarative font installation, no more dragging.ttffiles into Font Bookservices.yabai/services.skhd— tiling window managementservices.sketchybar— custom menu bar replacementservices.jankyborders— window borders for tiling setupslaunchd.user.agents/launchd.daemons— custom launchd services for anythingprograms.fish/programs.zsh— shell configurationpower— sleep and wake settingssystem.defaults.universalaccess— accessibility settingssystem.defaults.WindowManager— Stage Manager configurationsystem.defaults.controlcenter— control center visibilitynetworking.dns/networking.search— DNS configurationsystem.defaults.smb— SMB/network settings
The full picture
The point isn’t any single option. It’s that your Mac — the preferences, the services, the packages, the Dock, the keyboard, the shell — is described in one place. You can diff it, review it, roll it back, and hand it to a new machine.
The best way to discover what’s available is the nix-darwin option search. Type a keyword, see what’s declarative. You’ll be surprised how much of System Settings has a Nix equivalent.
darwin-rebuild switch and your Mac is exactly the machine you described.