con un clic
modules
Create and modify NixOS and Home-Manager modules
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Menú
Create and modify NixOS and Home-Manager modules
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Basado en la clasificación ocupacional SOC
Hardens systemd services against security issues using sandboxing, capability restriction, and syscall filtering. Use when creating or modifying systemd services, writing NixOS modules with systemd.services, reviewing service security, or whenever a serviceConfig block is involved. Ensures services follow defense-in-depth principles with minimal privilege.
Write Nix code using repo conventions
Write and review Conventional Commits commit messages (v1.0.0) for semantic versioning and changelogs. Use when drafting git commit messages, PR titles, release notes, or enforcing conventional commit format like `type(scope): subject`, `BREAKING CHANGE`, footers, and `revert`.
Manages version control with Jujutsu (jj), including rebasing, conflict resolution, and Git interop. Use when tracking changes, navigating history, squashing/splitting commits, or pushing to Git remotes.
Manage background jobs, capture command output, and handle session multiplexing. Use when running long commands, capturing output from detached processes, or managing concurrent tasks in headless environments.
Create terminal screenshots and GIFs with VHS tape files. Use when automating terminal recordings, capturing TUI screenshots, or generating demo GIFs.
| name | modules |
| description | Create and modify NixOS and Home-Manager modules |
Standard module pattern:
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mkEnableOption mkOption mkIf types;
cfg = config.services.myService;
in
{
options.services.myService = {
enable = mkEnableOption "my service";
port = mkOption {
type = types.port;
default = 8080;
description = "Port to listen on.";
};
};
config = mkIf cfg.enable {
# Configuration applied when enabled
};
}
Create file at modules/nixos/<category>/<name>.nix
Define options and config:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.myService;
in
{
options.services.myService = {
enable = lib.mkEnableOption "my service";
};
config = lib.mkIf cfg.enable {
systemd.services.my-service = {
wantedBy = [ "multi-user.target" ];
serviceConfig.ExecStart = "${pkgs.myPackage}/bin/my-service";
};
};
}
Register in parent default.nix:
# modules/nixos/services/default.nix
_: {
imports = [
./existing-service.nix
./my-service.nix # Add this
];
}
Enable in host config:
# hosts/server/myhost/default.nix
{ services.myService.enable = true; }
Create file at modules/home-manager/<category>/<name>.nix
Define with optional osConfig access:
{
osConfig ? null,
config,
lib,
pkgs,
...
}:
let
cfg = config.purpose.myFeature;
in
{
options.purpose.myFeature = {
enable = lib.mkEnableOption "my feature";
};
config = lib.mkIf cfg.enable {
home.packages = [ pkgs.myPackage ];
};
}
Register in parent default.nix
Enable in user config:
# home/<user>/hm-config.nix
{ purpose.myFeature.enable = true; }
| Namespace | Module Type | Purpose |
|---|---|---|
services.<name> | NixOS | System services |
hardware.<name> | NixOS | Hardware configuration |
boot.<name> | NixOS | Boot configuration |
host.<name> | NixOS | Host-specific options |
server.<name> | NixOS | Server cluster options |
core.<name> | Home-Manager/NixOS | Opinionated configurations & features |
purpose.<category> | Home-Manager | Use-case modules |
user.<name> | Home-Manager | User-specific options |
# modules/nixos/default.nix
{
boot = import ./boot;
hardware = import ./hardware;
services = import ./services;
}
# modules/nixos/services/default.nix
_: {
imports = [
./service-a.nix
./service-b.nix
];
}
project-structure skillnix evalnix fmt on changed filesWhen you need to add new options to a submodule defined in another module — without modifying the original file.
NixOS's module system merges attrsOf (submodule ...) declarations from
multiple modules. Two modules declaring the same options. path with
compatible submodule types will have their inner options attrsets
merged additively.
# Module A: core/options.nix — defines the submodule
options.server.proxy.virtualHosts = mkOption {
type = attrsOf (submodule ({ name, ... }: {
options = {
port = mkOption { type = port; description = "Backend port."; };
extraConfig = mkOption { type = str; default = ""; };
};
}));
};
# Module B: extensions/kanidm.nix — injects new options WITHOUT touching A
options.server.proxy.virtualHosts = mkOption {
type = attrsOf (submodule ({ name, ... }: {
options.kanidm = mkOption {
type = nullOr (submodule { ... });
default = null;
};
}));
};
After merge, every virtualHost entry has port, extraConfig, AND kanidm.
imports in the submodule?A common intuition is to have each extension set a vhostModule field and
have the submodule collect them in its imports:
# Fails: circular dependency
submodule ({ config, ... }: {
imports =
config.extensions
|> builtins.attrValues
|> map (ext: ext.vhostModule);
})
This creates infinite recursion — reading config inside imports
means the submodule type depends on config values that haven't been resolved.
Nix detects this and throws "infinite recursion: you probably reference config in imports".
options merge works vs. imports| Approach | Works? | Use when |
|---|---|---|
Declare same options. path with attrsOf (submodule ...) | ✅ Yes | Adding new options to existing submodule entries |
Dynamic imports in submodule reading config | ❌ No | Circular — config needs submodule type, import needs config |
Static imports in submodule (no config access) | ✅ Yes | Importing known modules, no conditional logic |
Wrap the whole config in mkIf inside imported submodule | ✅ Yes | Conditional config generation (but options always declared) |
attrsOf (submodule ...), not one str and one submodule.enable. Use mkIf in config to control behavior.submodule and submoduleWith — the inner options
attrsets union at type-resolution time, before config evaluation.systemd.services, nginx.virtualHosts,
and others compose submodule options from multiple modules.See the proxy extension registry for a complete example:
modules/nixos/server/proxy/options.nix — declares core vhost submodulemodules/nixos/server/proxy/extensions/kanidm.nix — injects kanidm option + owns all kanidm logic (auth, globalConfig, provisioning)modules/nixos/server/proxy/extensions/dashboard.nix — no vhost options neededmodules/nixos/server/proxy/extensions/cloudflared.nix — no vhost options neededProblem: A module loaded on all device types sets options that only exist on servers (e.g. server.proxy.virtualHosts). mkIf does not help — the NixOS module system resolves the attribute path in the definition body even when the condition is false. On a desktop, server.proxy doesn't exist → evaluation fails.
imports gated on deviceType specialArgmkSystem passes deviceType via specialArgs (like importExternals), so it is available at module level without touching config — no circular dependency.
deviceType in the module's function parameters.imports.mkIf normally since it only loads on matching device types.# modules/nixos/ai/services/mnemosyne.nix
{
config,
pkgs,
lib,
deviceType ? null,
...
}:
{
imports = lib.optionals (deviceType == "server") [ ./mnemosyne-caddy.nix ];
...
}
# The standalone submodule (mnemosyne-caddy.nix)
{
config,
lib,
...
}:
let
cfg = config.services.mnemosyne;
in
lib.mkIf (cfg.enable && cfg.caddy.enable) {
server.proxy.virtualHosts = lib.mkMerge [ ... ];
}
# lib/builders/mkSystem.nix — deviceType passed as specialArg
specialArgs = {
inherit deviceType;
...
};
imports + config?# Fails: infinite recursion
{
config, ...
}: {
imports = lib.optionals (config.host.device.role == "server") [ ./caddy.nix ];
}
Reading config inside a module's imports creates a circular dependency:
config needs all imports resolved first, but the import list depends on config.
deviceType from specialArgs avoids this — it's a plain value, not a thunk into config.
config that references options from a device-type-specific namespace (server.*, hardware.*)attrValues allModules (all device types)If the device-gated config is small, declare a stub option in a core module that always loads:
# modules/nixos/core/default.nix
options.server.proxy.virtualHosts = lib.mkOption {
type = lib.types.attrsOf lib.types.anything;
default = { };
};
Then mkIf works normally. Downside: pollutes the option namespace on non-server hosts, and the stub type might accept invalid config silently. Prefer the imports + deviceType pattern for anything non-trivial.