Everything You Can Set on macOS with nix-darwin

6 min read

Most 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 .ttf files into Font Book
  • services.yabai / services.skhd — tiling window management
  • services.sketchybar — custom menu bar replacement
  • services.jankyborders — window borders for tiling setups
  • launchd.user.agents / launchd.daemons — custom launchd services for anything
  • programs.fish / programs.zsh — shell configuration
  • power — sleep and wake settings
  • system.defaults.universalaccess — accessibility settings
  • system.defaults.WindowManager — Stage Manager configuration
  • system.defaults.controlcenter — control center visibility
  • networking.dns / networking.search — DNS configuration
  • system.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.