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.
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.
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.
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 |
config = mkIf (!isDarwin) {
users.defaultUserShell = pkgs.zsh; # Darwin sees this path!
};
config = optionalAttrs (!isDarwin) {
users.defaultUserShell = pkgs.zsh; # Hidden from Darwin
};
# cfg.flavor evaluated at parse time → infinite recursion
(optionalAttrs (isDarwin && cfg.flavor == "personal") {
services.onepassword-secrets.enable = true;
})
# Platform check at parse time, config check lazy
(optionalAttrs isDarwin (mkIf (cfg.flavor == "personal") {
services.onepassword-secrets.enable = true;
}))
options.modules.foo = {
user = mkOpt types.str config.user.name; # Infinite recursion!
};
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
});
For modules with both platform-specific options AND config-dependent behavior:
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;
}))
]);
| 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 |
When you see infinite recursion errors mentioning _module.freeformType or anon-43:
config. references in option defaultscfg. references in optionalAttrs conditionsmkIf (!isDarwin) or mkIf isDarwin guarding platform-specific options# Find problematic patterns
grep -rn "mkOpt.*config\." modules/
grep -rn "optionalAttrs.*cfg\." modules/
grep -rn "mkIf.*isDarwin" modules/
npx skills add edmundmiller/nix-platform-specific-options下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
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.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer