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.
Best use case
nix is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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.
Teams using nix should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/nix/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How nix Compares
| Feature / Agent | nix | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
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.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# 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`)Related Skills
writing-clearly-and-concisely
Apply Strunk's timeless writing rules to ANY prose humans will read - documentation, commit messages, error messages, explanations, reports, or UI text. Makes your writing clearer, stronger, and more professional.
web-search
Web search using DuckDuckGo (free, unlimited). Falls back to pi-web-access extension for content extraction.
web-browser
Interact with web pages using agent-browser CLI. MUST run 'browser connect 9222' FIRST to use existing browser with authenticated sessions.
tmux
Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output.
ticket-worker
Work on a single tk ticket end-to-end. Use when the user says 'work on ticket X' or when spawned by work-tickets.sh.
ticket-creator
Create and refine tickets for the tk ticket system. Use when the user says 'create tickets for X', 'refine ticket X', 'break this into tickets', 'seed tickets from plan', or anything about creating or refining tk tickets.
tell
Delegate tasks to other agents - pi sessions or external agents (claude, opencode, aider). Non-blocking with task tracking and completion notifications.
task-pipeline
Structured workflow for research → plan → tickets → work. Use when starting or continuing a task with /task, /plan, or /tickets commands.
preview
Display code, diffs, images, and other content in a tmux pane or popup. Auto-detects nvim/megaterm for floating popups.
mcpctl
Manage MCP server configurations — add, remove, list, inspect, troubleshoot. Use when asked to "add mcp server", "remove mcp", "list mcp servers", "mcp status", "configure mcp", "troubleshoot mcp", or any MCP server management task.
handoff
Save session state for later pickup. Use /handoff when context is degrading, /pickup to resume in a new session.
github
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.