docs/skills/nix/SKILL.md
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.
npx skillsauth add megalithic/dotfiles nixInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are a Nix expert specializing in:
~/.dotfiles/ (flake-based)just rebuild (uses workaround script, see below)nix search nixpkgs#<package> or nh search <query>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.
~/.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)
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 │
└─────────────────────────────────────────────────────────────┘
# 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')
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
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.
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
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/
| Command | Package Name |
|---------|--------------|
| rg | ripgrep |
| fd | fd |
| bat | bat |
| code | vscode |
| subl | sublime4 |
# 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
# 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.
# 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
# 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
Use the web interface to search for home-manager options:
https://home-manager-options.extranix.com/?query=<search-term>
Examples:
https://home-manager-options.extranix.com/?query=programs.githttps://home-manager-options.extranix.com/?query=programshttps://home-manager-options.extranix.com/?query=xdgUse WebFetch tool to query this URL when helping the user find home-manager configuration options.
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
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>
# 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>
# 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>
options.services.myservice = {
enable = lib.mkEnableOption "my service";
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Port to listen on";
};
};
# 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 ]
# 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 ])
# 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...";
}
# 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.git = {
enable = true;
userName = "Name";
extraConfig = {
init.defaultBranch = "main";
};
};
home.activation.myScript = lib.hm.dag.entryAfter ["writeBoundary"] ''
# Shell script here
mkdir -p $HOME/.local/share/myapp
'';
system.defaults = {
dock.autohide = true;
finder.AppleShowAllFiles = true;
NSGlobalDomain = {
AppleKeyboardUIMode = 3;
InitialKeyRepeat = 15;
KeyRepeat = 2;
};
};
homebrew = {
enable = true;
onActivation.cleanup = "zap";
brews = [ "mas" ];
casks = [ "firefox" ];
masApps = { "Xcode" = 497799835; };
};
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 listlib.mega.mkMas - Install Mac App Store appslib.mega.mkAppActivation - Symlink apps to /Applicationslib.mega.brewAlias - Create wrappers for Homebrew binarieslib.mega.capitalize - Capitalize first letter of stringlib.mega.compactAttrs - Filter null values from attrsetlib.mega.imports - Smart module path resolutionThe mkApp function in lib/mkApp.nix supports three install methods. ALWAYS verify which method is needed before choosing.
| 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 |
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:
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!
}
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)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
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
}
| 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/binconfig.lib.mega.linkDotfile "path" - Generic dotfiles symlinklib.mkDefault for overridable defaultslib.mkForce sparingly (only when necessary)lib.mkIf over inline conditionals for claritysha256-...) not old hex format--show-tracenix-prefetch-* tools to get correct hashnix log /nix/store/<drv> for build logsmacOS 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
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
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;
};
};
Before adding packages to any flake, verify its structure:
# 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"
# 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
# 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)
# 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
];
};
}
);
}
# 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>
home.file vs xdg.configFile - former is $HOME/, latter is ~/.config/mkOutOfStoreSymlink requires absolute path at eval timesystem.*, not services.* for most thingsenvironment.systemPackages is system-wide, home.packages is per-userripgrep not rg), or check NURmeta.platforms - some packages don't build on darwinflake.nix exists and git-tracked (git add flake.nix)testing
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.
tools
Web search using DuckDuckGo (free, unlimited). Falls back to pi-web-access extension for content extraction.
tools
Interact with web pages using agent-browser CLI. MUST run 'browser connect 9222' FIRST to use existing browser with authenticated sessions.
tools
Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output.