Install
$ agentstack add skill-rikdc-ai-skills-nix-expert ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Nix Expert
Advisory guidance on Nix, NixOS, and home-manager. This skill answers questions, explains patterns, and reviews Nix code.
Scope
Use this skill for:
- Explaining or reviewing Nix expressions and module definitions
- Authoring derivations and overlays
- Flake input management and lockfile hygiene
- Debugging build and evaluation failures
- home-manager configuration
- Secrets management patterns
- NixOS security hardening
When the task is to author or change .nix files for a specific host, check whether the target repository ships its own config-generation skill and prefer it — it will know that flake's layout, secrets backend, and conventions, which this skill deliberately does not assume. Failing that, read the existing tree before writing anything, and conform to what is already there.
Routing
Read the matching reference file rather than answering from this file when the request is specific. Each one is a task-oriented procedure, not passive background:
| Topic | Trigger | Reference | |-------|---------|-----------| | Build | "build nix package", "nixos-rebuild build", "compile nix" | [references/Build.md](references/Build.md) | | Debug | "debug nix", "nix error", "evaluation error", "infinite recursion" | [references/Debug.md](references/Debug.md) | | Develop | "development shell", "nix develop", "devShell", "direnv" | [references/Develop.md](references/Develop.md) | | Deploy | "deploy nixos", "nixos-rebuild switch", "remote deployment" | [references/Deploy.md](references/Deploy.md) | | Package | "create package", "derivation", "buildGoModule", "package app" | [references/Package.md](references/Package.md) | | Flakes | "create flake", "flake.lock", "update inputs", "flake outputs" | [references/Flakes.md](references/Flakes.md) | | Secrets | "manage secrets", "agenix", "encrypt secrets", "age encryption" | [references/Secrets.md](references/Secrets.md) | | Security | "harden nixos", "apparmor", "firewall", "security hardening" | [references/Security.md](references/Security.md) | | Troubleshoot | "hash mismatch", "nix failing", "common errors" | [references/Troubleshoot.md](references/Troubleshoot.md) |
Read only the one you need. For general guidance, continue with this file.
Core Principles
1. Declarative over imperative
# Good
services.nginx.enable = true;
# Bad — imperative escape hatch
systemd.services.nginx.postStart = "systemctl start nginx";
2. Reproducibility
Same inputs produce the same outputs. Pin versions explicitly, commit flake.lock, and avoid impure operations (builtins.getEnv, --impure, unpinned fetchTarball).
3. Modularity
# Good
imports = [
./hardware.nix
./networking.nix
./services.nix
];
4. Version control everything
Track all Nix configuration in git. Always put the nix flake update output in the commit message body — a flake.lock diff is bare hashes and epochs, so the generated summary is the only readable record of which inputs moved and where. nix flake update --commit-lock-file does this for you. Add the why above it when an input is pinned or overridden.
5. Prefer flakes
Flakes give hermetic evaluation, a standard output schema, dependency locking, and better caching.
NixOS Configuration Patterns
Host configuration structure
hosts//
├── default.nix # Imports modules, host-specific config
├── boot.nix # Bootloader, initrd, kernel modules
└── hardware.nix # Hardware settings, filesystems, mounts
Layouts vary between repositories. Read the existing tree before assuming one.
Shared module organization
modules/
├── common/ # Baseline imported by every host
├── profiles/ # Opt-in role profiles (bare-metal, vm-guest, nvidia)
└── services/ # One file per service
A mkHost helper
Many flakes wrap host construction to avoid repetition:
nixosConfigurations = {
some-host = libx.mkHost {
hostname = "some-host";
system = "x86_64-linux";
profile = "bare-metal";
};
};
If the repository has such a helper, use it rather than calling nixpkgs.lib.nixosSystem directly.
Module Best Practices
Define options properly
{ config, lib, pkgs, ... }:
{
options.services.myservice = {
enable = lib.mkEnableOption "my service";
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Port to listen on";
};
configFile = lib.mkOption {
type = lib.types.path;
description = "Path to configuration file";
};
};
config = lib.mkIf config.services.myservice.enable {
# Implementation
};
}
Choosing a type
The full set is documented in the NixOS manual under Options Types and defined in lib/types.nix in nixpkgs. Read one of those rather than working from memory — the set grows, and composed types (either, oneOf, coercedTo, attrTag) are easy to misremember.
Pick the narrowest type that fits: types.port rather than types.int, types.enum rather than types.str when the value set is closed. The module system then rejects bad values at eval time instead of at activation.
mkIf, mkMerge, mkDefault
config = lib.mkIf config.services.myservice.enable { };
config = lib.mkMerge [
{ always.present = true; }
(lib.mkIf condition { conditional.value = true; })
];
# Overridable default
services.myservice.port = lib.mkDefault 8080;
mkDefault has priority 1000, mkForce has 50, a plain assignment has 100. Use mkForce sparingly — it defeats the merge system.
Package Development
Derivation templates, the callPackage pattern, and per-language builders (buildGoModule, rustPlatform, python3Packages) live in [references/Package.md](references/Package.md). Read that rather than working from memory — meta attributes and hash arguments are easy to get subtly wrong.
Overlays
{ inputs }:
{
additions = final: _prev: import ../pkgs { pkgs = final; };
modifications = final: prev: {
somepackage = prev.somepackage.overrideAttrs (old: {
version = "custom";
});
};
}
Use final for anything that should see later overlays, prev for the input you are modifying. Referencing final.foo inside a definition of foo causes infinite recursion.
Flake Management
nix flake lock # Lock dependencies
nix flake update # Update all inputs
nix flake update nixpkgs # Update one input
nix flake check # Validate outputs
nix flake show # List outputs
nix flake metadata # Show inputs and revision
{
description = "Flake description";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs }: {
nixosConfigurations = { };
homeConfigurations = { };
packages = { };
devShells = { };
};
}
Home-Manager Patterns
Session variables
home.sessionVariables = {
EDITOR = "vim";
BROWSER = "firefox";
};
XDG config files
# Symlink a static file
xdg.configFile."myapp/config.yml".source = ./myapp-config.yml;
# Generate dynamically
xdg.configFile."myapp/generated.conf".text = ''
setting1 = ${someValue}
'';
# Executable
xdg.configFile."bin/script.sh" = {
source = ./script.sh;
executable = true;
};
User services
systemd.user.services.myservice = {
Unit = {
Description = "My Service";
After = [ "network.target" ];
};
Service = {
ExecStart = "${pkgs.mypackage}/bin/myservice";
Restart = "on-failure";
};
Install.WantedBy = [ "default.target" ];
};
Secrets Management
Never put plaintext secrets in a .nix file — everything in the Nix store is world-readable. Use agenix or sops-nix, and reference the decrypted path at runtime.
# secrets.nix
let
user = "ssh-ed25519 AAAAC3...";
system = "ssh-ed25519 AAAAC3...";
in {
"secret.age".publicKeys = [ user system ];
}
{
age.secrets.mySecret = {
file = ../secrets/mySecret.age;
owner = "myuser";
group = "mygroup";
};
# Pass the path, never the value
services.myservice.passwordFile = config.age.secrets.mySecret.path;
}
agenix -e secrets/mySecret.age # Edit or create
agenix -r # Re-key after adding a host
See [references/Secrets.md](references/Secrets.md) for the full treatment.
Safety and Testing
switch is the only irreversible-in-the-moment step. Climb the ladder in order and stop at the first failure — never skip a rung to save time.
| # | Command | Proves | On failure | |---|---------|--------|------------| | 1 | nixos-rebuild dry-build --flake .# | Evaluates and shows what would be built | Fix eval errors; re-run 1 | | 2 | nixos-rebuild build --flake .# | It actually compiles | Read the failing derivation; re-run 2 | | 3 | nixos-rebuild dry-activate --flake .# | Which units would restart or stop | Reconsider blast radius; back to 1 | | 4 | nixos-rebuild test --flake .# | Activates now, bootloader untouched | reboot recovers the old generation | | 5 | nixos-rebuild switch --flake .# | Activates and makes it the boot default | nixos-rebuild switch --rollback |
Rung 3 is the one people skip, and the one that catches "this restarts postgresql and sshd" before it happens.
Confirm each rung actually succeeded before advancing — check the exit status, don't assume. If rung 2 fails, rungs 3-5 are meaningless.
Extra care on remote hosts
Never switch a remote host you cannot physically reach without first running rungs 1-4. A bad networking or openssh change locks you out, and rung 5 makes it survive the reboot that would otherwise have saved you.
# Build locally, push the closure, activate with a safety net
nixos-rebuild test --flake .# --target-host root@ --build-host localhost
Consider services.openssh.enable and firewall changes one-at-a-time, and keep a second SSH session open while testing.
Rollback
nixos-rebuild list-generations # See what you can return to
nixos-rebuild switch --rollback # Previous generation
nixos-rebuild switch --switch-generation N # A specific one
Keep at least two or three recent generations. nix-collect-garbage -d deletes every rollback target — never run it as a first response to a full disk, and never immediately after a switch you have not yet verified.
See [references/Deploy.md](references/Deploy.md) for remote deployment, and [references/Troubleshoot.md](references/Troubleshoot.md) when a rung fails.
Common Patterns
Conditional imports
imports = [
./base.nix
] ++ lib.optionals (desktop != null) [
./desktop/${desktop}
];
String interpolation
message = "Hello ${name}";
config = ''
setting1 = ${value1}
'';
# Escaping $ inside an indented string
script = ''
echo "Nix variable: ${nixVar}"
echo "Shell variable: ''${shellVar}"
'';
List and attrset operations
all = list1 ++ list2;
filtered = lib.filter (x: x > 5) list;
doubled = map (x: x * 2) list;
merged = set1 // set2; # Shallow
merged = lib.recursiveUpdate set1 set2; # Deep
filtered = lib.filterAttrs (n: v: v != null) attrs;
mapped = lib.mapAttrs (n: v: v * 2) attrs;
Debugging
value = lib.traceVal someExpression;
value = lib.traceSeq "message" someExpression;
nix eval .#nixosConfigurations..config.services.nginx.enable
nix derivation show .#package
nix path-info -Sh .#package
nix why-depends .#package .#dependency
Add --show-trace to any failing evaluation for the full stack.
Hash mismatch
nix-prefetch-github owner repo --rev
For Go modules, set vendorHash = lib.fakeHash; and read the correct hash out of the build failure.
Infinite recursion
Usually a config value read at the top level of a module, or an overlay referring to final.foo while defining foo. Move the read inside config or switch to prev.
Performance
- Use binary caches; add project caches via
nixConfig.extra-substituters - Keep
flake.lockupdated but stable — churn invalidates caches - Use
nix-direnvso dev shells are cached and GC-rooted - Avoid expensive list operations inside module option defaults
Security
Quick wins
{
networking.firewall.enable = true;
services.openssh.settings = {
PermitRootLogin = "no";
PasswordAuthentication = false;
};
system.autoUpgrade = {
enable = true;
allowReboot = false;
};
security.apparmor.enable = true;
}
Checklist
- [ ] Firewall enabled, default deny
- [ ] SSH hardened — no root login, no password auth
- [ ] Secrets encrypted, referenced by path not value
- [ ] Automatic updates configured
- [ ] AppArmor or per-service systemd hardening enabled
- [ ] Audit logging on critical services
- [ ] Minimal package set on exposed hosts
See [references/Security.md](references/Security.md) for the hardened profile and per-service sandboxing.
Resources
- NixOS Manual:
- Nixpkgs Manual:
- Home-Manager Manual:
- Nix Pills:
- Option search:
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: rikdc
- Source: rikdc/ai-skills
- License: MPL-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.