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.
git clone https://github.com/megalithic/dotfiles
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"
docs/skills/nix/SKILL.mdNix 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:
(flake-based)~/.dotfiles/ - Rebuild command:
(uses workaround script, see below)just rebuild - Package search:
ornix search nixpkgs#<package>nh search <query>
CRITICAL: Rebuild Command
ALWAYS use
instead of just rebuild
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
. Always use Nix.brew install
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
| Command | Package Name |
|---|---|
| |
| |
| |
| |
| |
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
(flake-level):lib/default.nix
- Build macOS apps from DMG/ZIP/PKG (see detailed guide below)lib.mega.mkApp
- Build multiple apps from a listlib.mega.mkApps
- Install Mac App Store appslib.mega.mkMas
- Symlink apps to /Applicationslib.mega.mkAppActivation
- Create wrappers for Homebrew binarieslib.mega.brewAlias
- Capitalize first letter of stringlib.mega.capitalize
- Filter null values from attrsetlib.mega.compactAttrs
- Smart module path resolutionlib.mega.imports
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 |
|---|---|---|
(default) | Most apps - DMG, ZIP, or simple PKG | |
| Apps with system extensions | + enable service |
| Mac App Store apps | Either |
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:
-
If output shows ONLY
→ Use extract method./Applications/SomeApp.app/*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):
(DriverKit)./Library/SystemExtensions/*
or./Library/LaunchDaemons/*./Library/LaunchAgents/*./Library/PrivilegedHelperTools/*
(privileged binaries)./usr/local/bin/*
-
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
| App | Method | Reason |
|---|---|---|
| TalkTastic | | PKG only contains |
| Fantastical | | Standard ZIP with .app bundle |
| Brave Browser | | Standard DMG with .app bundle |
| Karabiner-Elements | | Has DriverKit virtual HID extension |
| Little Snitch | | Has network kernel extension |
In
(home-manager module, via home/lib.nix
):config.lib.mega
- Symlink toconfig.lib.mega.linkConfig "path"~/.dotfiles/config/{path}
- Symlink toconfig.lib.mega.linkHome "path"~/.dotfiles/home/{path}
- Symlink toconfig.lib.mega.linkBin~/.dotfiles/bin
- Generic dotfiles symlinkconfig.lib.mega.linkDotfile "path"
Best Practices
- Use
for overridable defaultslib.mkDefault - Use
sparingly (only when necessary)lib.mkForce - Prefer
over inline conditionals for claritylib.mkIf - Use SRI hashes (
) not old hex formatsha256-... - Pin flake inputs for reproducibility
- Use overlays for package modifications, not inline overrides
- Separate concerns: system config in modules/, user config in home/
Debugging Tips
- Infinite recursion: Usually caused by self-referential options. Use
--show-trace - Attribute not found: Check spelling, imports, and that module is loaded
- Hash mismatch: Use
tools to get correct hashnix-prefetch-* - Build failures: Check
for build logsnix log /nix/store/<drv> - "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
vshome.file
- former isxdg.configFile
, latter is$HOME/~/.config/
requires absolute path at eval timemkOutOfStoreSymlink- Darwin modules use
, notsystem.*
for most thingsservices.*
is system-wide,environment.systemPackages
is per-userhome.packages- Package not found: Try different names (
notripgrep
), or check NURrg - Platform unsupported: Check
- some packages don't build on darwinmeta.platforms - Flake not recognized: Ensure
exists and git-tracked (flake.nix
)git add flake.nix