nix

$npx mdskill add megalithic/dotfiles/nix

Provides expert assistance with Nix ecosystem tools for configuration, package management, and debugging on macOS.

  • Helps users manage dotfiles, install packages, develop modules, and resolve evaluation errors.
  • Integrates with Bash, file operations, web fetching, and web search tools.
  • Relies on user environment details like platform and rebuild commands to offer tailored advice.
  • Delivers results through command recommendations, path guidance, and specific workarounds for known issues.

SKILL.md

.github/skills/nixView on GitHub ↗
---
name: nix
description: 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.
tools: Bash, Read, Grep, Glob, Edit, Write, WebFetch, WebSearch
---

# 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:

```bash
# 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

```bash
# 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:

```bash
# 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:

```nix
# 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`

```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:

```bash
# 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

| Command | Package Name |
|---------|--------------|
| `rg` | `ripgrep` |
| `fd` | `fd` |
| `bat` | `bat` |
| `code` | `vscode` |
| `subl` | `sublime4` |

## Common Tasks

### 1. Validate Configuration

```bash
# 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

```bash
# 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

```bash
# 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

```bash
# 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:

```bash
# 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:

```bash
# 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

```bash
# 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

```bash
# 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)

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

### Conditional Attributes

```nix
# 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

```nix
# 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

```nix
# 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

```nix
# 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

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

### Activation Scripts

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

## Darwin-Specific

### System Defaults

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

### Homebrew Integration

```nix
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

| Method | Use Case | Config 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 apps | Either |

### How to Determine the Correct Method for PKG Files

**IMPORTANT: Most PKG files do NOT need native installation!**

```bash
# 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**
   ```nix
   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:
   ```bash
   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):**
```nix
# 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):**
```nix
# 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!):**
```nix
# 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

| App | Method | Reason |
|-----|--------|--------|
| 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:

```bash
# 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`:
```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

```bash
# 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

```bash
# 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

```bash
# 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

```bash
# Initialize with template
nix flake init

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

# Minimal flake.nix for a dev environment:
```

```nix
{
  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

```bash
# 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`)

More from megalithic/dotfiles

SkillDescription
brave-searchWeb search and content extraction via Brave Search API. Use for searching documentation, facts, or any web content. Lightweight, no browser required.
cli-toolsModern CLI tool usage (fd, rg) for fast file and content searching. Critical for Nix store searches and large codebases. Use when searching files or content, especially in /nix/store.
hsComprehensive guide for Hammerspoon development in this dotfiles repo. Covers config patterns, debugging decision trees, API reference, performance monitoring, and troubleshooting.
image-handlingImage handling for Claude API constraints (5MB max, 8000px max dimension). Use when working with images, screenshots, or MCP browser tools.
jjJujutsu (jj) version control workflow, commands, and best practices. Use when working with version control in jj-enabled repos. Covers commits, bookmarks, workspaces, and safe push patterns.
notesExpert help with the meganote system - cross-tool note capture, daily notes, and obsidian.nvim integration. Covers Hammerspoon, Shade, nvim, and the full capture → daily note linking pipeline.
nvimComprehensive guide for Neovim configuration in this dotfiles repo. Covers plugin management, LSP debugging, treesitter, keymaps, performance, and troubleshooting decision trees.
previewDisplay code, diffs, images, and other content in a tmux pane or popup. Auto-detects nvim/megaterm for floating popups.
shadeExpert help with Shade - the native Swift note capture app. Use for debugging Shade issues, understanding IPC protocols, implementing Hammerspoon integration, nvim RPC, context gathering, and meganote workflows.
smart-ntfySend intelligent notifications via ~/bin/ntfy with context-aware channel selection. Use when completing tasks, asking questions, encountering errors, or reaching milestones.