Agent skill
nix-platform-specific-options
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.
Install this agent skill to your Project
npx add-skill https://github.com/edmundmiller/dotfiles/tree/main/.agents/skills/nix-platform-specific-options
SKILL.md
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
config = mkIf (!isDarwin) {
users.defaultUserShell = pkgs.zsh; # Darwin sees this path!
};
✅ Correct: optionalAttrs for platform check
config = optionalAttrs (!isDarwin) {
users.defaultUserShell = pkgs.zsh; # Hidden from Darwin
};
❌ Wrong: Config value in optionalAttrs condition
# cfg.flavor evaluated at parse time → infinite recursion
(optionalAttrs (isDarwin && cfg.flavor == "personal") {
services.onepassword-secrets.enable = true;
})
✅ Correct: Nest mkIf inside optionalAttrs
# 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
options.modules.foo = {
user = mkOpt types.str config.user.name; # Infinite recursion!
};
✅ Correct: Static default, use config in config section
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:
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:
- Search for
config.references in option defaults - Search for
cfg.references inoptionalAttrsconditions - Search for
mkIf (!isDarwin)ormkIf isDarwinguarding platform-specific options
# Find problematic patterns
grep -rn "mkOpt.*config\." modules/
grep -rn "optionalAttrs.*cfg\." modules/
grep -rn "mkIf.*isDarwin" modules/
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
zbench
Benchmark interactive zsh performance with zsh-bench and track regressions. Use when benchmarking shell startup, comparing zsh latency after config changes, investigating slow shell, or running git bisect on performance. Trigger phrases: "benchmark zsh", "shell is slow", "zbench", "zsh-bench", "shell startup time", "profile zsh", "zsh performance".
nix-rebuild
Rebuild nix-darwin/NixOS system after dotfiles changes. Use when config files managed by Nix (lazygit, ghostty, etc.) need to be regenerated, or after editing any .nix file in the dotfiles repo.
hass-config-flow
Interact with Home Assistant via the REST API on a NixOS host. Use when adding integrations, querying entities, managing config flows, creating API tokens, or automating HA setup programmatically. Also covers identifying device protocols (Matter, Zigbee, Thread, HomeKit) from the device registry. Trigger phrases: "add HA integration", "configure home assistant", "query HA entities", "create HA token", "HA REST API", "pair homekit", "set up matter in HA", "add spotify to HA", "is this device zigbee or thread", "what protocol is this device", "move devices to ZHA", "identify matter devices".
hass-declarative
Manage Home Assistant automations, scenes, and scripts declaratively via NixOS modules. Covers adding/editing/removing entities in the domain-based Nix structure, the ensureEnabled wrapper (initial_state enforcement), the sweep service that cleans orphaned entities, entity identity (IDs, slugs, unique_ids), the eval test assertions, and the build-time manifest. Trigger phrases: "add HA automation", "new scene", "new script", "remove automation", "declarative HA", "sweep unmanaged", "entity drift", "ghost entity", "orphaned automation", "HA domain file", "eval-automations test", "hass assertion", "ensureEnabled", "initial_state".
agenix-secrets
Create, edit, and wire up agenix-encrypted secrets in this dotfiles repo. Use when adding API keys, tokens, credentials, passwords, or any sensitive values to NixOS host configs. Trigger phrases: "add a secret", "encrypt with agenix", "new age secret", "hide this value", "agenix secret".
linear
Read-only Linear issue access via the Linear GraphQL API.
Didn't find tool you were looking for?