Skip to content
Back to skills

Nix Platform Specific Options

ASecurity

Write Nix modules with platform-specific options (NixOS vs Darwin) without infinite recursion. Use when mkIf causes evaluation errors or options don't exist across platforms.

  • 81 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 8, 2026
toolsshellbashdebugging

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add edmundmiller/dotfiles --skill nix-platform-specific-options --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Nix Platform Specific Options?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Nix Platform Specific Options
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/edmundmiller-nix-platform-specific-options/badge)](https://www.skillsdirectory.com/skills/edmundmiller-nix-platform-specific-options)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: nix-platform-specific-options
description: Write Nix modules with platform-specific options (NixOS vs Darwin) without infinite recursion. Use when mkIf causes evaluation errors or options don't exist across platforms.
---

# Nix Module Platform-Specific Options

## Overview

When writing Nix modules that need to hide platform-specific options (NixOS vs Darwin), using `mkIf` alone causes infinite recursion. This skill documents the correct pattern.

## The Problem

`mkIf` is evaluated lazily but the option **path** is still visible during module evaluation. This causes errors like:

```
error: The option `users.defaultUserShell' does not exist.
```

Or infinite recursion when `config` is referenced in option defaults or `optionalAttrs` conditions.

## The Pattern

**Use `optionalAttrs` for platform checks, `mkIf` for config-dependent checks.**

| Check Type                                 | Tool            | Evaluated  |
| ------------------------------------------ | --------------- | ---------- |
| Platform (`isDarwin`, `!isDarwin`)         | `optionalAttrs` | Parse time |
| Config values (`cfg.enable`, `cfg.flavor`) | `mkIf`          | Lazy       |

## Examples

### ❌ Wrong: mkIf for platform check

```nix
config = mkIf (!isDarwin) {
  users.defaultUserShell = pkgs.zsh;  # Darwin sees this path!
};
```

### ✅ Correct: optionalAttrs for platform check

```nix
config = optionalAttrs (!isDarwin) {
  users.defaultUserShell = pkgs.zsh;  # Hidden from Darwin
};
```

### ❌ Wrong: Config value in optionalAttrs condition

```nix
# cfg.flavor evaluated at parse time → infinite recursion
(optionalAttrs (isDarwin && cfg.flavor == "personal") {
  services.onepassword-secrets.enable = true;
})
```

### ✅ Correct: Nest mkIf inside optionalAttrs

```nix
# Platform check at parse time, config check lazy
(optionalAttrs isDarwin (mkIf (cfg.flavor == "personal") {
  services.onepassword-secrets.enable = true;
}))
```

### ❌ Wrong: config reference in option default

```nix
options.modules.foo = {
  user = mkOpt types.str config.user.name;  # Infinite recursion!
};
```

### ✅ Correct: Static default, use config in config section

```nix
options.modules.foo = {
  user = mkOpt types.str null;
};

config = mkIf cfg.enable (let
  user = if cfg.user != null then cfg.user else config.user.name;
in {
  # Use 'user' variable here
});
```

## Combined Pattern

For modules with both platform-specific options AND config-dependent behavior:

```nix
config = mkIf cfg.enable (mkMerge [
  # Common config (all platforms)
  { /* ... */ }

  # Darwin-only options
  (optionalAttrs isDarwin {
    programs.zsh.interactiveShellInit = "...";
  })

  # NixOS-only options
  (optionalAttrs (!isDarwin) {
    users.defaultUserShell = pkgs.zsh;
  })

  # Darwin + config-dependent (nested)
  (optionalAttrs isDarwin (mkIf (cfg.flavor == "personal") {
    services.onepassword-secrets.enable = true;
  }))
]);
```

## Quick Reference

| Scenario                   | Pattern                                                    |
| -------------------------- | ---------------------------------------------------------- |
| NixOS-only option          | `optionalAttrs (!isDarwin) { ... }`                        |
| Darwin-only option         | `optionalAttrs isDarwin { ... }`                           |
| Platform + enable check    | `optionalAttrs isDarwin (mkIf cfg.enable { ... })`         |
| Platform + config value    | `optionalAttrs isDarwin (mkIf (cfg.foo == "bar") { ... })` |
| Option default from config | Use `null` default, resolve in `config` section            |

## Debugging

When you see infinite recursion errors mentioning `_module.freeformType` or `anon-43`:

1. Search for `config.` references in option defaults
2. Search for `cfg.` references in `optionalAttrs` conditions
3. Search for `mkIf (!isDarwin)` or `mkIf isDarwin` guarding platform-specific options

```bash
# Find problematic patterns
grep -rn "mkOpt.*config\." modules/
grep -rn "optionalAttrs.*cfg\." modules/
grep -rn "mkIf.*isDarwin" modules/
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.