Skip to content

Repository files navigation

nix-config

Personal Nix configuration for macOS using Nix Flakes, nix-darwin, home-manager, and nix-homebrew.

Quick Links:


What & Why

Problem: Setting up new dev machines is tedious and error-prone. Installing 50+ tools, recreating years of customizations, dealing with version conflicts between projects. Every setup takes 4-8 hours and something is always forgotten.

Solution: This nix-config uses Nix flakes to make my entire system reproducible. One command sets up everything exactly how I like it. Projects get isolated environments via direnv + nix, so no more "works on my machine" issues.

Reality Check: I only set up 2-3 new machines per year, but I switch between projects daily. The real win is seamless project environment switching and fearless experimentation.

Architecture

See docs/architecture.md for technical details.

What's Included

CLI Tools (50+):

  • Modern replacements: eza (ls), bat (cat), fd (find), ripgrep (grep)
  • Shell: ZSH with ZIM framework, Powerlevel10k prompt
  • Dev tools: Git with delta, fzf everywhere, direnv
  • Custom packages: alias-teacher (helps learn my own aliases), mysides (Finder sidebar management)

Development Environments:

  • Go/Rust/Claude dev shells via nix-devx (dev <name> wrapper)
  • Project isolation via direnv + flake.nix (this is the real magic)
  • Pre-commit hooks, formatters, linters

System Features:

  • TouchID for sudo
  • Weekly garbage collection
  • Unified theming (Dracula via base24)
  • GNU utils prepended to PATH (sanity on macOS)

Via Homebrew (nix-homebrew manages these):

  • GUI apps: Bitwarden, Firefox, iTerm2, JetBrains Toolbox, etc.
  • Mac App Store: Bear, Noir, AdGuard for Safari

Setup

First-time setup on a fresh Mac. I only do this 2-3 times a year, so it's written down.

Prerequisites

  1. Xcode Command Line Tools (for git):

    xcode-select --install
  2. Rosetta 2 — optional, for x86 apps on Apple Silicon:

    softwareupdate --install-rosetta --agree-to-license
  3. Nix package manager:

    sh <(curl -L https://nixos.org/nix/install)
  4. Clone this repo:

    git clone https://github.com/screwyprof/nix-config.git ~/nix-config
    cd ~/nix-config

First Build

nix-darwin needs to take over the shell rc files, so move the defaults aside first:

sudo mv /etc/bashrc /etc/bashrc.before-nix-darwin
sudo mv /etc/zshrc  /etc/zshrc.before-nix-darwin

# Bootstrap nix-darwin with this flake (the host is "macbook"):
sudo nix --extra-experimental-features "nix-command flakes" run nix-darwin -- switch --flake '.#macbook'

Every rebuild after that just uses the alias:

nix-rebuild-host      # darwin-rebuild switch --flake .#macbook

Post-Install

  • Projects symlink: the build creates ~/Projects → ~/Documents/Projects. It starts working once that folder exists (and iCloud finishes syncing, if you sync Documents).
  • Safari extensions: enable Noir and AdGuard for Safari in Safari → Settings → Extensions. The build installs them from the App Store, but macOS requires toggling them on by hand.

Commands I Actually Use

# System management
nix-rebuild-host      # Rebuild macbook
nix-check             # Run flake checks + pre-commit hooks
nix-update            # Update all flake inputs
nix-fmt               # Format Nix files
nix-cleanup           # Garbage collect + optimize

# Development (via nix-devx)
dev go                # Go dev shell
dev rust              # Rust dev shell
dev claude            # Claude + MCP servers (restricted)
dev claude-unrestricted  # Claude + MCP (skip permissions)

# Set NIX_DEVX=/path/to/nix-devx for local clone, otherwise fetches from GitHub
# Reality: I just create flake.nix per project
# Just cd into any project with flake.nix and direnv does the rest

devbox surfaces (Linux)

Two homes the Mac config doesn't cover: the devbox node and the cage I actually work in. Neither has a host config here — the node's NixOS config lives in the devbox repo, and a cage's system closure is built by devbox — so both are standalone homeConfigurations, applied by hand.

Once the node config is active, both have aliases (from dev-nix, Linux-only) — run from this repo:

nix-rebuild-devbox           # the node's own home
nix-rebuild-cage payment     # a cage's /home/dev

The long forms, and what to use the first time:

# The node's own home (/home/happygopher.guest)
nix build .#homeConfigurations.devbox-host.activationPackage && ./result/activate

# A cage's /home/dev — build on the node, activate INSIDE the cage.
# Pass the STORE PATH, not ./result: a cage binds /nix/store but not this repo,
# so a result symlink here is invisible in there.
OUT=$(nix build --no-link --print-out-paths .#homeConfigurations.devbox-cage.activationPackage)
sudo machinectl shell dev@<project> /run/current-system/sw/bin/bash -lc "$OUT/activate"

Standalone has no -b backup flag. If activation refuses with "would be clobbered", move the files aside first — mv ~/.bashrc ~/.bashrc.pre-hm — then re-run. Worth reading the list before you do: an undeclared line in ~/.config/git/ignore was nearly lost that way (it's declared in dev-git now).

zsh needs the handoff, not a chsh. Both the node and the cage log you into bash, and the cage's shell is set by devbox's security floor, which deliberately carries no user preferences. So home-manager supplies zsh in the user profile and programs.bash.initExtra execs it for interactive shells only — scripts, ssh <host> <command> and devbox's session rail stay on bash.

Extensions differ by surface. The node gets its VS Code set from here (base + rust, via vscode-sets.nix), because nothing else manages that home. A cage does not — devbox owns ~/.vscode-server/extensions per project, placing the set declared in the project's session flake before the editor attaches. Two owners, disjoint paths.

Honest Trade-offs

Accepted:

  • 50GB Nix store — Complete reproducibility
  • High complexity — But AI makes it manageable now
  • macOS-only — Optimized for what I actually use
  • Some maintenance time — Worth it for daily productivity
  • ~10min rebuilds — Acceptable for full system updates

Rejected:

  • Cross-platform purity (this is macOS-only)
  • Team-friendly (this is MY config, use at your own risk)
  • Minimal/simple (I want all my tools configured perfectly)

Lessons Learned

  1. Complexity creeps: Each tool integration led to another
  2. Documentation matters: Git commits aren't enough context
  3. Perfection trap: Some things are good enough
  4. AI changes everything: Nix complexity less scary with Claude — this is THE game changer that makes Nix viable for personal use
  5. YAGNI applies to infra too: Multi-user, multi-platform support removed when not actually used

Technical Choices Worth Remembering

  • ZIM over Oh-My-Zsh: Performance matters when you live in the terminal
  • nix-darwin + home-manager: System + user separation is worth the complexity
  • direnv: The magic that makes project switching seamless
  • Weekly GC: Automated via launchd, keeps store from growing infinitely
  • Flake-parts partitions: Dev tooling doesn't slow down system evaluation

This README is for future me who's forgotten how everything works. Hi future me! The answers you seek are in the git history, AI assistants, and that docs/DECISIONS.md file you hopefully created. Don't overthink it.

About

Personal nix config with nix-darwin and home-manager following nix dendritic pattern

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages