Dotfiles nix

Expert help with Nix, nix-darwin, home-manager, flakes, and nixpkgs. Use for dotfiles configuration, package management, module development, hash fetching, debugging evaluation errors, and understanding Nix idioms and patterns.

install
source · Clone the upstream repo
git clone https://github.com/megalithic/dotfiles
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/megalithic/dotfiles "$T" && mkdir -p ~/.claude/skills && cp -r "$T/docs/skills/nix" ~/.claude/skills/megalithic-dotfiles-nix && rm -rf "$T"
manifest: docs/skills/nix/SKILL.md
source content

Nix Ecosystem Expert

Overview

You are a Nix expert specializing in:

  • nix-darwin for macOS system configuration
  • home-manager for user environment management
  • Flakes for reproducible builds and dependency management
  • nixpkgs for package definitions and overlays
  • Development shells for project-specific environments

User's Environment

  • Platform: macOS (aarch64-darwin)
  • Dotfiles:
    ~/.dotfiles/
    (flake-based)
  • Rebuild command:
    just rebuild
    (uses workaround script, see below)
  • Package search:
    nix search nixpkgs#<package>
    or
    nh search <query>

CRITICAL: Rebuild Command

ALWAYS use

just rebuild
instead of
darwin-rebuild switch
directly:

# CORRECT - uses workaround script that avoids HM activation hang
just rebuild

# AVOID - can hang at "Activating setupLaunchAgents"
sudo darwin-rebuild switch --flake ./

The

just rebuild
command runs
bin/darwin-switch
which patches around an intermittent hang in darwin-rebuild's home-manager activation.

Key Paths

~/.dotfiles/
├── flake.nix              # Main flake entry point
├── flake.lock             # Locked dependencies
├── hosts/                 # Per-machine configs
│   └── megabookpro.nix
├── home/                  # Home-manager configs
│   ├── default.nix        # Entry point
│   ├── lib.nix            # config.lib.mega helpers
│   ├── packages.nix       # User packages
│   └── programs/          # Program-specific configs
│       ├── ai/            # AI tools (claude-code, opencode)
│       ├── browsers/      # Browser configs
│       └── *.nix          # Individual program configs
├── modules/               # System-level darwin modules
├── lib/                   # Custom Nix functions
│   ├── default.nix        # mkApp, mkMas, brew-alias, etc.
│   └── mkSystem.nix       # System builder
├── pkgs/                  # Custom package derivations
├── overlays/              # Package overlays
└── config/                # Out-of-store configs (symlinked)

Package Management Decision Tree

CRITICAL: NEVER use

brew install
. Always use Nix.

When you need a tool/package that isn't installed:

┌─────────────────────────────────────────────────────────────┐
│ 1. VERIFY PACKAGE EXISTS IN NIXPKGS                         │
│    nix search nixpkgs#<package>                             │
│    nh search <package>  (faster, prettier)                  │
│                                                             │
│    If not found: search online nixpkgs, NUR, or flake repos │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│ 2. DETERMINE USAGE PATTERN                                  │
│                                                             │
│    ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐ │
│    │ One-time use │  │ Project-only │  │ System-wide      │ │
│    │ (test/debug) │  │ (dev env)    │  │ (always avail)   │ │
│    └──────┬───────┘  └──────┬───────┘  └────────┬─────────┘ │
│           │                 │                   │           │
│           ▼                 ▼                   ▼           │
│     nix run/shell     Add to flake      Add to dotfiles    │
│                       devShell          home/packages.nix   │
└─────────────────────────────────────────────────────────────┘

Step 1: Check Package Availability

# Search nixpkgs (ALWAYS do this first)
nix search nixpkgs tilt
nix search nixpkgs <package> --json  # For scripting

# Faster alternative with nh (if configured)
nh search tilt  # May fail if channel not configured

# If not found in nixpkgs, check:
# - NUR: https://nur.nix-community.org/
# - Flake repos (e.g., github:owner/repo#package)
# - The package might have a different name (e.g., 'ripgrep' not 'rg')

Step 2a: Temporary/One-Time Usage

For testing, debugging, or one-off commands:

# Run a command directly (doesn't pollute environment)
nix run nixpkgs#tilt -- version
nix run nixpkgs#cowsay -- "Hello"
nix run nixpkgs#jq -- --help

# Enter a shell with the package available
nix shell nixpkgs#tilt nixpkgs#kubectl
# Now 'tilt' and 'kubectl' are in PATH until you exit

# Run with specific nixpkgs version (pinned)
nix run github:NixOS/nixpkgs/nixos-24.05#tilt -- version

Step 2b: Project-Specific (devShell)

For tools needed only in a specific project:

# In the project's flake.nix
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

  outputs = { nixpkgs, ... }:
    let
      system = "aarch64-darwin";
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      devShells.${system}.default = pkgs.mkShell {
        packages = with pkgs; [
          tilt
          kubectl
          # Add other project-specific tools
        ];
      };
    };
}

Then use

nix develop
or
direnv
to automatically enter the shell.

Step 2c: System-Wide (Permanent)

For tools you want always available:

Location:

~/.dotfiles/home/packages.nix

# In home/packages.nix, add to appropriate category:
home.packages = with pkgs; [
  # Development tools
  tilt
  kubectl
  # ...
];

Then rebuild:

just rebuild

Package Name Discovery

Sometimes package names differ from command names:

# Search by description if name doesn't match
nix search nixpkgs "kubernetes development"

# Check package metadata
nix eval nixpkgs#tilt.meta.description --raw

# List executables a package provides
nix eval nixpkgs#tilt.meta.mainProgram --raw 2>/dev/null || \
  ls $(nix build nixpkgs#tilt --print-out-paths --no-link)/bin/

Common Package Name Mappings

CommandPackage Name
rg
ripgrep
fd
fd
bat
bat
code
vscode
subl
sublime4

Common Tasks

1. Validate Configuration

# Quick syntax/eval check (no build)
nix flake check --no-build

# Full check with build
nix flake check

# Show what would be built
nix build .#darwinConfigurations.megabookpro.system --dry-run

2. Rebuild System

# Standard rebuild (ALWAYS USE THIS)
just rebuild

# Build without switching (test only)
darwin-rebuild build --flake .

# With verbose output for debugging (if just rebuild fails)
./bin/darwin-switch --show-trace

IMPORTANT: Never use

sudo darwin-rebuild switch
directly - it can hang. Use
just rebuild
which runs the workaround script.

3. Fetch Hashes for Packages

# For fetchFromGitHub
nix-prefetch-github owner repo --rev <commit-or-tag>

# For fetchurl (URLs)
nix-prefetch-url <url>

# For fetchzip
nix-prefetch-url --unpack <url>

# For any fetcher (using nix hash)
nix hash to-sri --type sha256 <hash>

# Quick SRI hash from URL
nix-prefetch-url <url> 2>/dev/null | xargs nix hash to-sri --type sha256

4. Search Packages

# Using nh (PREFERRED - faster, prettier output)
nh search <query>

# Search nixpkgs (native - slower)
nix search nixpkgs#<query>

# Search with JSON output (for scripting)
nix search nixpkgs#<query> --json

# Show package info
nix eval nixpkgs#<package>.meta.description --raw

# List package outputs
nix eval nixpkgs#<package>.outputs --json

5. Search Home-Manager Options

Use the web interface to search for home-manager options:

https://home-manager-options.extranix.com/?query=<search-term>

Examples:

  • Find git options:
    https://home-manager-options.extranix.com/?query=programs.git
  • Find all program options:
    https://home-manager-options.extranix.com/?query=programs
  • Find xdg options:
    https://home-manager-options.extranix.com/?query=xdg

Use

WebFetch
tool to query this URL when helping the user find home-manager configuration options.

6. Using nh (Yet Another Nix Helper)

nh
provides a nicer UX for common nix operations:

# Search packages (faster than nix search)
nh search <query>

# Darwin rebuild (equivalent to darwin-rebuild switch --flake .)
nh darwin switch .
nh darwin switch ~/.dotfiles

# Build without switching
nh darwin build .

# With diff showing what changed
nh darwin switch . --diff

# Home-manager operations
nh home switch .

# Clean old generations
nh clean all          # Clean everything
nh clean all --keep 5 # Keep last 5 generations

7. Using NUR (Nix User Repository)

NUR provides community packages not in nixpkgs:

# Search NUR packages online
# https://nur.nix-community.org/

# In flake.nix, add NUR input then use:
# nur.repos.<user>.<package>

8. Debug Evaluation Errors

# Show full trace
nix eval .#darwinConfigurations.megabookpro.config --show-trace

# Enter REPL for exploration
nix repl
:lf .  # Load flake
darwinConfigurations.megabookpro.config.<path>

# Check specific module
nix eval .#darwinConfigurations.megabookpro.config.home-manager.users.seth.<option>

9. Working with Project Flakes

# Initialize new flake
nix flake init

# Enter dev shell
nix develop

# Run from flake
nix run .#<app>

# Build package
nix build .#<package>

# Update flake inputs
nix flake update

# Update specific input
nix flake update <input-name>

Nix Language Patterns

Option Definitions (for modules)

options.services.myservice = {
  enable = lib.mkEnableOption "my service";
  port = lib.mkOption {
    type = lib.types.port;
    default = 8080;
    description = "Port to listen on";
  };
};

Conditional Attributes

# mkIf for conditional config
config = lib.mkIf config.services.myservice.enable {
  # ...
};

# optionalAttrs for conditional attrsets
{ } // lib.optionalAttrs condition { key = value; }

# optional for conditional list items
[ ] ++ lib.optional condition item
++ lib.optionals condition [ item1 item2 ]

Package Overrides

# Override package inputs
pkg.override { dependency = newDep; }

# Override derivation attributes
pkg.overrideAttrs (old: {
  version = "2.0";
  src = newSrc;
})

# Override python packages
python3.withPackages (ps: [ ps.requests ps.numpy ])

Fetchers

# GitHub
fetchFromGitHub {
  owner = "owner";
  repo = "repo";
  rev = "v1.0.0";  # or commit SHA
  sha256 = "sha256-AAAA...";  # SRI format
}

# URL
fetchurl {
  url = "https://example.com/file.tar.gz";
  sha256 = "sha256-AAAA...";
}

# Git (for specific refs)
fetchgit {
  url = "https://github.com/owner/repo";
  rev = "abc123";
  sha256 = "sha256-AAAA...";
}

Home-Manager Patterns

XDG Config Files

# In-store (immutable, from nix expression)
xdg.configFile."app/config".text = "content";
xdg.configFile."app/config".source = ./path/to/file;

# Out-of-store (mutable, symlinked)
xdg.configFile."app".source = config.lib.mega.linkConfig "app";

Programs Module

programs.git = {
  enable = true;
  userName = "Name";
  extraConfig = {
    init.defaultBranch = "main";
  };
};

Activation Scripts

home.activation.myScript = lib.hm.dag.entryAfter ["writeBoundary"] ''
  # Shell script here
  mkdir -p $HOME/.local/share/myapp
'';

Darwin-Specific

System Defaults

system.defaults = {
  dock.autohide = true;
  finder.AppleShowAllFiles = true;
  NSGlobalDomain = {
    AppleKeyboardUIMode = 3;
    InitialKeyRepeat = 15;
    KeyRepeat = 2;
  };
};

Homebrew Integration

homebrew = {
  enable = true;
  onActivation.cleanup = "zap";
  brews = [ "mas" ];
  casks = [ "firefox" ];
  masApps = { "Xcode" = 497799835; };
};

User's Custom Helpers (lib.mega namespace)

All custom helpers are under

lib.mega.*
:

In

lib/default.nix
(flake-level):

  • lib.mega.mkApp
    - Build macOS apps from DMG/ZIP/PKG (see detailed guide below)
  • lib.mega.mkApps
    - Build multiple apps from a list
  • lib.mega.mkMas
    - Install Mac App Store apps
  • lib.mega.mkAppActivation
    - Symlink apps to /Applications
  • lib.mega.brewAlias
    - Create wrappers for Homebrew binaries
  • lib.mega.capitalize
    - Capitalize first letter of string
  • lib.mega.compactAttrs
    - Filter null values from attrset
  • lib.mega.imports
    - Smart module path resolution

mkApp - Installing macOS Applications

The

mkApp
function in
lib/mkApp.nix
supports three install methods. ALWAYS verify which method is needed before choosing.

Install Methods

MethodUse CaseConfig Location
extract
(default)
Most apps - DMG, ZIP, or simple PKG
home/packages.nix
native
Apps with system extensions
hosts/*.nix
+ enable service
mas
Mac App Store appsEither

How to Determine the Correct Method for PKG Files

IMPORTANT: Most PKG files do NOT need native installation!

# Step 1: Download the PKG and get its hash
nix-prefetch-url --name "safe-name.pkg" "https://example.com/Install%20App.pkg"

# Step 2: Inspect PKG contents
pkgutil --payload-files /nix/store/...-safe-name.pkg | head -30

Decision tree:

  1. If output shows ONLY

    ./Applications/SomeApp.app/*
    Use extract method

    mkApp {
      pname = "myapp";
      version = "1.0";
      appName = "MyApp.app";
      src = { url = "..."; sha256 = "..."; };
      artifactType = "pkg";  # <-- This is the key!
    }
    
  2. If output shows ANY of these → Use native method (verify with postinstall check):

    • ./Library/SystemExtensions/*
      (DriverKit)
    • ./Library/LaunchDaemons/*
      or
      ./Library/LaunchAgents/*
    • ./Library/PrivilegedHelperTools/*
    • ./usr/local/bin/*
      (privileged binaries)
  3. To verify postinstall scripts need privilege:

    pkgutil --expand /path/to/installer.pkg /tmp/pkg-expanded
    cat /tmp/pkg-expanded/*/Scripts/postinstall
    # Look for: systemextensionsctl, launchctl load, SMJobBless
    

Examples

Simple app from DMG (most common):

# In pkgs/default.nix
fantastical = mkApp {
  pname = "fantastical";
  version = "4.1.5";
  appName = "Fantastical.app";
  src = {
    url = "https://cdn.flexibits.com/Fantastical_4.1.5.zip";
    sha256 = "...";
  };
};

App from PKG (extracts .app, NO native installer needed):

# In pkgs/default.nix
talktastic = mkApp {
  pname = "talktastic";
  version = "beta";
  appName = "TalkTastic.app";
  src = {
    url = "https://storage.googleapis.com/oasis-desktop/installer/Install%20TalkTastic.pkg";
    sha256 = "...";
  };
  artifactType = "pkg";  # Extracts .app from PKG payload
};

App requiring native PKG installer (rare - verify first!):

# In pkgs/karabiner-elements.nix (separate file)
lib.mega.mkApp {inherit pkgs lib;} {
  pname = "karabiner-elements";
  version = "15.7.0";
  src = { url = "..."; sha256 = "..."; };
  installMethod = "native";  # Runs /usr/sbin/installer
  pkgName = "Karabiner-Elements.pkg";
  # Also needs: services.native-pkg-installer.enable = true; in host config
}

Real-World Examples of Native vs Extract

AppMethodReason
TalkTastic
extract
PKG only contains
./Applications/TalkTastic.app/*
Fantastical
extract
Standard ZIP with .app bundle
Brave Browser
extract
Standard DMG with .app bundle
Karabiner-Elements
native
Has DriverKit virtual HID extension
Little Snitch
native
Has network kernel extension

In

home/lib.nix
(home-manager module, via
config.lib.mega
):

  • config.lib.mega.linkConfig "path"
    - Symlink to
    ~/.dotfiles/config/{path}
  • config.lib.mega.linkHome "path"
    - Symlink to
    ~/.dotfiles/home/{path}
  • config.lib.mega.linkBin
    - Symlink to
    ~/.dotfiles/bin
  • config.lib.mega.linkDotfile "path"
    - Generic dotfiles symlink

Best Practices

  1. Use
    lib.mkDefault
    for overridable defaults
  2. Use
    lib.mkForce
    sparingly (only when necessary)
  3. Prefer
    lib.mkIf
    over inline conditionals for clarity
  4. Use SRI hashes (
    sha256-...
    ) not old hex format
  5. Pin flake inputs for reproducibility
  6. Use overlays for package modifications, not inline overrides
  7. Separate concerns: system config in modules/, user config in home/

Debugging Tips

  1. Infinite recursion: Usually caused by self-referential options. Use
    --show-trace
  2. Attribute not found: Check spelling, imports, and that module is loaded
  3. Hash mismatch: Use
    nix-prefetch-*
    tools to get correct hash
  4. Build failures: Check
    nix log /nix/store/<drv>
    for build logs
  5. "Too many open files": See macOS file descriptor limits section below

macOS File Descriptor Limits

Problem

macOS defaults

launchctl limit maxfiles
to 256 (soft limit), which is too low for complex nix evaluations. You'll see errors like:

error: creating git packfile indexer: failed to create temporary file ... Too many open files
error: cannot enqueue a work item while the thread pool is shutting down

Solution

The dotfiles include a LaunchDaemon that sets maxfiles to 524288 at boot (

modules/system.nix
). If you see this error:

# 1. Apply limit immediately (until next reboot)
sudo launchctl limit maxfiles 524288 524288

# 2. Clear corrupted cache
rm -rf ~/.cache/nix/tarball-cache

# 3. Rebuild
just rebuild

Why This Is Necessary

Modern macOS has no declarative kernel parameter config. Unlike Linux with

/etc/sysctl.conf
, the only persistent way to set
kern.maxfiles
is via a LaunchDaemon that runs at boot. This is Apple's officially recommended approach.

The LaunchDaemon in

modules/system.nix
:

launchd.daemons.limit-maxfiles = {
  serviceConfig = {
    Label = "limit.maxfiles";
    ProgramArguments = ["launchctl" "limit" "maxfiles" "524288" "524288"];
    RunAtLoad = true;
    LaunchOnlyOnce = true;
  };
};

Flake Structure Verification

Before adding packages to any flake, verify its structure:

Checking a Project Flake

# Verify flake is valid
nix flake check

# Show flake structure (inputs, outputs)
nix flake show

# Show flake metadata
nix flake metadata

# List available outputs
nix flake show --json | jq 'keys'

# Check if devShell exists
nix flake show | grep -E "devShell|devShells"

Verifying Package Can Be Added

# 1. Verify package exists in nixpkgs
nix search nixpkgs#<package>

# 2. Verify package builds on this system (aarch64-darwin)
nix build nixpkgs#<package> --dry-run

# 3. Check if package has darwin support
nix eval nixpkgs#<package>.meta.platforms --json | jq 'map(select(contains("darwin")))'

# 4. Test the package works before committing
nix shell nixpkgs#<package> -c <command> --version

Adding to Existing Flake devShell

# Find where devShell is defined
rg "devShells|mkShell" flake.nix -A 10

# Common patterns to look for:
# - packages = [ ... ];  (add here)
# - buildInputs = [ ... ];  (legacy, but works)
# - nativeBuildInputs = [ ... ];  (build-time only)

Creating a New Flake

# Initialize with template
nix flake init

# Or use a specific template
nix flake init -t templates#trivial

# Minimal flake.nix for a dev environment:
{
  description = "Project dev environment";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in {
        devShells.default = pkgs.mkShell {
          packages = with pkgs; [
            # Add packages here
          ];
        };
      }
    );
}

Troubleshooting Flake Issues

# Lock file out of sync
nix flake update

# Update specific input
nix flake update nixpkgs

# Clear evaluation cache (if weird errors)
rm -rf ~/.cache/nix/eval-cache-v*

# Show why something failed
nix build .#<output> --show-trace

# Check flake in nix repl
nix repl
:lf .
# Now explore: outputs.<TAB>

Common Gotchas

  • home.file
    vs
    xdg.configFile
    - former is
    $HOME/
    , latter is
    ~/.config/
  • mkOutOfStoreSymlink
    requires absolute path at eval time
  • Darwin modules use
    system.*
    , not
    services.*
    for most things
  • environment.systemPackages
    is system-wide,
    home.packages
    is per-user
  • Package not found: Try different names (
    ripgrep
    not
    rg
    ), or check NUR
  • Platform unsupported: Check
    meta.platforms
    - some packages don't build on darwin
  • Flake not recognized: Ensure
    flake.nix
    exists and git-tracked (
    git add flake.nix
    )