Nix configuration for my macOS systems and home environment.
The configuration is split into two independent layers:
| Layer | Purpose | Command |
|---|---|---|
| nix-darwin | macOS system config (defaults, homebrew, dock) | nix run .#switch-darwin |
| home-manager | User environment (shell, git, tmux, neovim, packages) | nix run .#switch-home |
These are fully independent — changing your shell config doesn't require a darwin rebuild, and vice versa.
# 1. Install Nix
curl -fsSL https://install.determinate.systems/nix | sh -s -- install
# 2. Bootstrap (blocking, expects input from you)
nix run --refresh github:andreykaipov/nix#bootstrap -- <host>
# 3. Build and switch everything
cd ~/gh/nix && nix run .#switch
# Subsequent rebuilds (from anywhere)
switch [darwin|home]- macOS on Apple Silicon (aarch64-darwin)
curl -fsSL https://install.determinate.systems/nix | sh -s -- installDeterminate Nix
comes with flakes enabled by default and manages the Nix daemon itself. Because
of this, nix.enable = false is set in the nix-darwin config — nix-darwin
won't try to manage the daemon, settings, or garbage collection.
Sets the hostname, generates a per-host SSH key, and uploads it to GitHub — all in one command:
nix run .#bootstrap <host>Give the host a distinguishable name, like airfryer, or toaster, or goonermania.
The hostname determines which host config under hosts/ is used. If the host's
config doesn't exist yet, the script creates one automatically.
The bootstrap script will:
- Set the machine hostname
- Generate a new host under
hosts - Generate a per-host SSH key at
~/.ssh/<host>.pem - Upload the public key to GitHub (the
ghCLI will prompt you) - Commit these changes back up to
~/gh/nix
nix run .#switch-darwinThis will prompt for your sudo password. On the first run, darwin-rebuild
isn't in your PATH yet, so the script bootstraps it via
nix run nix-darwin -- switch. After the first run, darwin-rebuild is
available directly.
This:
- Configures macOS system defaults (keyboard, Finder, trackpad, security)
- Installs homebrew and all casks/brews (GUI apps go into
/Applications/) - Sets up the Dock layout
- Launches Rectangle (and any other apps that need a first run to register their login items)
Note: GUI apps are installed via homebrew casks, not nix packages. Home-manager app linking into
/Applications/is disabled.
nix run .#switch-homeOn the first run, home-manager isn't in your PATH yet, so the script
bootstraps it via nix run home-manager. After the first run,
home-manager switch --flake .#<hostname> is available directly.
This sets up:
- zsh with powerlevel10k
- git config
- SSH config and keys
- tmux
- neovim (with auto-installed plugins via MiniDeps)
- direnv + nix-direnv
- User packages (dev tools, LSPs, etc.)
- User scripts in
~/bin - WezTerm terminal config
Or run both steps at once:
nix run .#switchYou can store secret environment variables that get sourced by zsh on
shell startup. Create a file named zshenv.<name> under
modules/home/shell/config/ — for example:
# modules/home/shell/config/zshenv.work
export CORP_API_TOKEN=sk-abc123
export WORK_NPM_TOKEN=npm_xyz789These plaintext files are gitignored. They live only on the machine where
you create them. On shell startup, zsh sources all zshenv* files from
the config directory.
The bootstrap script will create a host config for you automatically, but if you want to do it yourself, you can.
- Create a directory under
hosts/matching the machine's hostname:
mkdir -p hosts/my-machine-
Using an existing host as inspiration, add a
default.nixwith at least thesystemandusername. -
Set the machine's hostname to match, then run
nix run .#switch.
.
├── flake.nix # Flake entrypoint
├── lib/ # Helper functions (mkConfig, mkApp, discoverModules)
├── hosts/ # Per-host config (system, username, publicKey, extraModules)
├── modules/
│ ├── darwin/ # Core nix-darwin modules (system defaults, homebrew, dock, etc.)
│ ├── home/ # Core home-manager modules (shell, git, ssh, tmux, nvim, etc.)
│ └── extra/ # Opt-in modules, picked per-host via extraModules
│ └── dev/ # Development tools (go, lua, nix, terraform, cloud, docker, etc.)
└── apps/
└── aarch64-darwin/ # App scripts (switch, switch-darwin, switch-home, bootstrap, etc.)
Core modules under modules/home/ and modules/darwin/ are auto-discovered
and applied to every host. Extra modules under modules/extra/ are opt-in —
each host chooses what it needs via extraModules.
To add a new extra module, create a directory under modules/extra/ with a
home.nix and/or darwin.nix. It will be auto-discovered as lib.extras.<name>.
Nested directories are supported (e.g., modules/extra/dev/go/home.nix becomes
lib.extras.dev.go). Parent directories aggregate their children, so
lib.extras.dev includes all dev sub-modules at once.
By default neovim uses mini.hues' randomhue — a random palette that stays
consistent across all sessions until the next nix run .#switch-home (the seed
is written at switch time). To re-roll the colorscheme without switching:
colorscheme # from the shell — writes a new seed + updates tmux/weztermOr press <leader>cc inside neovim to re-roll interactively (reloads the colorscheme if not using randomhue).
To pin a specific colorscheme instead of randomhue, set theme.colorscheme.name
in your host config (hosts/<name>/default.nix) and run nix run .#switch-home.
Run colorschemes to list all available names.
Colorscheme options under theme.colorscheme:
| Option | Default | Description |
|---|---|---|
name |
randomhue |
Colorscheme name (or randomhue for random palette) |
lighterShade |
30 |
Percent to lighten bg for inactive panes/splits |
blackBg |
false |
Force black background on Normal/SignColumn/LineNr |
These are written to ~/.config/nvim/host.lua on nix switch and can be
edited live without rebuilding.
Tmux theme options under theme.tmux:
| Option | Values | Description |
|---|---|---|
pane |
subtle / chunky |
Pane border style |
border |
all / none |
Border visibility |
bg |
inactive / active |
Terminal bg source (dimmed or Normal) |
These are written to ~/.config/tmux/styles/host.conf on nix switch and can
be edited live without rebuilding. Changes take effect on tmux config reload
(prefix r). See color sync order for how they flow
through the stack.
nvim is the color source — it generates the colorscheme (e.g. randomhue) and propagates colors to tmux and wezterm. The full chain involves all four layers (nix, nvim, tmux, wezterm) interacting at different times:
On nix switch:
- Activation script writes
~/.config/nvim/host.lua(colorscheme name, tmux theme settings) - Activation script writes
~/.config/tmux/styles/host.conf(pane style, border style, status bg) - nvim runs headless (
nvim --headless +qa) which triggers the colorscheme sync plugin — this writes~/.config/tmux/styles/nvim-colors.confwith the generated color values
On tmux startup (or config reload):
core.confsourcesstyles/host.conf— sets@host_pane_style,@host_pane_border_style,@host_status_bgcore.confsourcesstyles/main.conf— readsstyles/nvim-colors.confto set@nvim_color_*options, then applies all tmux styles using those values
On nvim startup (or colorscheme change):
tmux-colorscheme-syncplugin repaints pane bg insetup()(prevents black flash)- Plugin reads nvim highlight groups → sets
@nvim_color_*tmux options → writesstyles/nvim-colors.conf→ re-sourcesstyles/main.conf(if colors changed) sync_terminal_bgsends OSC 11 to set wezterm's terminal background color
On wezterm startup:
- Reads
~/.config/tmux/styles/nvim-colors.confto set initial background color (before nvim starts)
On transparency toggle (Cmd+U):
- Wezterm sets
@transparenttmux option and re-sourcesstyles/main.conf styles/main.confapplies transparent or opaque styles based on@transparent- nvim transparency is toggled independently via
<leader>u
# System changes (macOS defaults, homebrew, dock, etc.)
nix run .#switch-darwin
# Home environment changes (shell, packages, dotfiles, etc.)
nix run .#switch-home
# Both at once
nix run .#switchIn any darwin module, but most likely would go in modules/darwin/homebrew/default.nix:
homebrew.casks = [
"docker-desktop"
"wezterm"
"my-new-app" # add here
];Then nix run .#switch-darwin.
In any home module, but most likely would go in modules/home/packages/default.nix:
home.packages = with pkgs; [
ripgrep
my-new-tool # add here
];Then nix run .#switch-home.
Home-manager modules can symlink their ~/.config/<name> directory back into
the repo so edits take effect immediately without rebuilding:
{ host, ... }:
{
xdg.configFile."nvim" = host.symlinkTo ./.;
}This creates ~/.config/nvim → ~/gh/nix/modules/home/nvim. The directory name
is derived from the path you pass in (./. resolves to the current module's
directory). See hosts/extend.nix for implementation details.
The flake.lock pins every input to a specific revision. Inputs share
nixpkgs via follows so there's only one copy in the closure — but the
pinned revision can go stale. Update regularly:
# Update everything (nixpkgs, home-manager, neovim-nightly, etc.)
nix flake update
# Update just nixpkgs (e.g. to pick up a new package version)
nix flake update nixpkgs
# Update a single input
nix flake update home-managerAfter updating, rebuild with nix run .#switch to activate the new versions.
Commit the updated flake.lock so other machines get the same pins.
Tip: If a build fails after updating nixpkgs, check whether a transitive dependency (like
llm-agents) needs a package that only exists in a newer nixpkgs. Updating nixpkgs to the latest unstable usually fixes it.
nix run .#clean # removes generations older than 30 daysnix run .#rollback # lists generations, prompts for which to restore