Nix Agent
Overview
nix-agent MCP tools do NOT read or write files. They build, diff, switch,
and manage generations (the operational core), and evaluate, locate, and
check the live config (introspection), handing every result back as a
compact JSON envelope. Read the envelope: it already holds what you would
otherwise re-fetch by hand.
Division of labor:
- Your native tools (Read/Edit/Write) edit
.nix files.
mcp-nixos discovers packages and options (what exists, what it means).
nix-agent operates on the user's actual configuration (what their machine resolves, whether it builds, what a switch would change).
Default is apply: after a config change, diff() then switch() without
waiting. Wait only when the user asked to preview or check, not activate.
Tool Surface
Seven tools in two tiers. All auto-resolve the target when flake_uri is
omitted and echo back what they resolved and ran (resolved_target and
command, or commands for check("lint")).
Operational core:
build(flake_uri?, mode?): build the closure, no activation. A failed
build carries failed_derivation.
diff(flake_uri?, mode?): what a switch would change (adds/removes/
version bumps), plus a structured packages object when it parses.
Include this in the reply and switch unless the user asked only to
preview or check.
switch(flake_uri?, mode?, validate?, full_log?): activate. Records
rollback_generation, returns a summary (units changed, derivations
built, packages vs the rollback generation, health), and trims the
log to a tail on success. validate=True gates on check("dry-build").
Remote refs and pin mismatches exit as remote_ref_rejected /
target_locked before sudo or dry-build. . / ./ resolve to the
pin when $NIX_AGENT_FLAKE is set. Newly failed units after
activation are status: "degraded"; report summary.health and roll
back unless the user asked to leave those units failed. Privileged
argv uses sudo -n.
generations(action="list"|"rollback", mode?, generation?): list or
roll back. NixOS list entries include path when the profile link
exists. After switch, call
generations(action="rollback", generation=<rollback_generation or id>).
Bare generations(action="rollback") is previous-generation only.
Unknown ids return unknown_generation and run no command.
Config introspection:
eval_config(attr, flake_uri?, mode?): final merged value of any
config attribute on THIS machine, after all modules/overlays.
mcp-nixos says what an option means; this says what it is. Pass a
list for attr to evaluate many in one call (per-attr results).
All attrs ok or mixed → ok (failures in results); every attr
failed → failed with first_error from the first failed entry that
has one. Values above ~2 KB degrade to attr names / length / a head
slice, marked truncated: true. A missing hostname attr falls back to
a unique flake host or returns unknown_host with hosts. In NixOS
mode, a missing option is retried as home-manager.users.<user>.<attr>
(hm_rewritten: true).
locate_option(attr, flake_uri?, mode?): which file sets an option,
as declarations and definitions ({file, value} per file). Earns
its slot as that which-file answer, not as a cap (measured
environment.systemPackages is 24 KB → 20 KB). Use this instead of
grepping the tree. status is not_an_option for plain config values
(use eval_config there). For integrated HM, spell the attr
home-manager.users.<user>.<attr> with mode="nixos"; if you pass
the unprefixed attr, the tool retries that spelling (hm_rewritten).
Missing hostname → unknown_host with hosts.
check(level, flake_uri?, mode?): validation ladder, fast to slow:
"lint" (statix + deadnix, structured findings), "dry-build",
"dry-activate" (NixOS only). Dry-activate uses the same remote/pin
classifier as switch and attaches privilege on sudo auth failure.
Formatting is not a tool: format edited files with the flake's own
formatter (nix fmt, or nixfmt on the files) via your Bash tool. Repo
onboarding is the nix-agent inspect-flake CLI subcommand, not a tool; the
nix-agent-init skill drives it.
Picking mode (read before any HM change)
mode defaults to "nixos". Do NOT reflexively switch to
"home-manager" just because the task touches Home Manager options.
That is the most common way to operate on the wrong config.
- Integrated HM (wired in as a NixOS module via
home-manager.nixosModules.home-manager + home-manager.users.*):
no separate home-manager switch. HM is built and activated as part
of the system closure, so use mode="nixos" (the default) and
switch the whole system. The common laptop/desktop layout.
- Standalone HM (its own flake exposing
homeConfigurations.*,
applied with home-manager switch): use mode="home-manager".
If both a NixOS flake and a standalone ~/.config/home-manager flake
exist on the machine, the standalone one is often vestigial. Confirm
which is actually active (eval_config against each, or check what the
running generation was built from) before mutating. When in doubt,
mode="nixos" is the safer guess.
For nonstandard or multi-flake layouts, do not rely on auto-resolution:
set NIX_AGENT_FLAKE once (or NIX_AGENT_HM_FLAKE for standalone HM,
which falls back to NIX_AGENT_FLAKE), or pass an explicit flake_uri
like /home/you/nixos#host. Either pins the target exactly.
Wrong-host symptom: unknown_host lists hosts from the flake. A
failed envelope whose first_error names a missing
nixosConfigurations."<hostname>" means auto-resolution could not
recover; pass an explicit flake_uri (.../repo#realhost) or
$NIX_AGENT_FLAKE, not by retrying the same hostname.
Workflow
- Discovery (if needed): query
mcp-nixos for packages/options;
eval_config for what the machine currently resolves; locate_option
for which file to open.
- Edit
.nix files with your native file tools.
- Format with the flake's formatter (
nix fmt, or nixfmt on the edited files) via Bash, then check("lint"): fix findings worth fixing.
check("dry-build"): catches eval/build errors cheaply.
diff(): include the changeset in the reply and switch unless the
user asked only to preview or check.
switch(): report the result and rollback_generation. Keep that
value. status: "degraded" means activation succeeded but units
newly failed — report summary.health and roll back unless the user
asked to leave those units failed.
- On failure at any step: read
first_error, then error_detail, then
failed_derivation.log_tail; fix and retry. status: "preflight_failed"
means switch(validate=True) never activated — fix the nested
preflight dry-build, do not retry activation. A privilege field
means sudo auth failed, not a Nix error. unknown_generation,
unknown_host, target_locked, and remote_ref_rejected are early
exits: no command ran. On regret after a switch:
generations(action="rollback", generation=<rollback_generation or id>).
Bare generations(action="rollback") is previous-generation only,
and is only the right default when nothing else has switched since.
Steps 3 through 5 are judgment calls, not gates. For a trivial change,
going straight to switch is fine.
Token discipline: the envelope is the interface
This server pre-digests Nix's firehose. An agent that re-runs nix log
or systemctl after every operation throws that away. Read the fields;
do not re-fetch.
- On failure, read three fields in order and stop.
first_error is
the actionable line. error_detail is {message, file, line, column, trace} when Nix emitted an eval error: a direct file:line:column edit
target. failed_derivation.log_tail is the failing builder's log,
already fetched. Do NOT run nix log or re-run with full_log=True
unless these fields are absent.
- After a switch, read
summary, do not re-probe. status: "degraded"
means units newly_failed; treat that as a problem, not success, and
roll back with
generations(action="rollback", generation=<rollback_generation>)
unless the user asked to leave those units failed.
summary.health reports units newly_failed/resolved/still_failed with journal
tails for the first five newly failed units; summary.packages reports changes
vs the rollback generation. These replace running systemctl --failed
or a second diff().
- Batch attr checks.
eval_config([...]) answers N questions in one
call. Mixed results stay ok with failures in results; if every
attr failed, top-level status is failed with first_error from
the first failed entry that has one. A truncated: true value means
eval a child attr for the part you need, NOT retry for full output.
locate_option before grepping. It answers "which file sets this"
in one call; a tree-wide grep does not. That is why it has a slot, not
because it caps a firehose (24 KB → 20 KB on
environment.systemPackages).
- Byte accounting is off the envelope.
raw_bytes / returned_bytes
live in the usage log (NIX_AGENT_USAGE_LOG=1, then nix-agent usage),
not on the tool response. Do not look for them in MCP results.
- Escape hatches are deliberate last resorts.
full_log=True and the
raw output field exist for the rare case the trimmed view genuinely
lacks what you need; reaching for them by default defeats the server.
Onboarding a repo
First time in an unfamiliar config? Run /nix-agent-init: it runs
nix-agent inspect-flake once and generates AGENT_MAP.md, CLAUDE.md
(+ an AGENTS.md symlink), and .mcp.json from the observed facts, never
boilerplate.
Hard Rules
- Never write secret payloads into config files; reference secrets via
sops-nix/agenix and only edit references.
- Never call
switch when the user asked only to check or preview;
diff is the preview. Otherwise the default is apply: switch after
diff() without waiting.
status: "degraded" means activation succeeded but units newly
failed; report summary.health and roll back unless the user asked
to leave those units failed.
- Host allowlists cannot see
flake_uri. Privileged tools reject remote
refs and honor $NIX_AGENT_FLAKE / $NIX_AGENT_HM_FLAKE as an
anti-footgun (the HM lock does not fall back to the NixOS pin). Do not
treat the pin as a security boundary. Sudoers must be narrowed to that
directory. Default install is unprompted activation plus passwordless
sudo for this flake. Do not re-ask for confirmation. Lower trust
(host prompts, sudo password) only if the user asked.
- After
switch, undo with
generations(action="rollback", generation=<rollback_generation>).
Bare rollback is previous-only.
1---2name: nix-agent3description: Operate on this machine's NixOS or Home Manager configuration with the nix-agent MCP tools (build, diff, switch, generations, eval_config, locate_option, check). Use when changing packages, options, or modules; diagnosing a failed build or switch; rolling back a generation; or asking what the live config currently resolves to. Requires the nix-agent MCP server (usually alongside mcp-nixos).4---56# Nix Agent78## Overview910`nix-agent` MCP tools do NOT read or write files. They build, diff, switch,11and manage generations (the operational core), and evaluate, locate, and12check the live config (introspection), handing every result back as a13compact JSON envelope. Read the envelope: it already holds what you would14otherwise re-fetch by hand.1516Division of labor:17- **Your native tools** (Read/Edit/Write) edit `.nix` files.18- **`mcp-nixos`** discovers packages and options (what exists, what it means).19- **`nix-agent`** operates on the user's actual configuration (what their machine resolves, whether it builds, what a switch would change).2021Default is apply: after a config change, `diff()` then `switch()` without22waiting. Wait only when the user asked to preview or check, not activate.2324## Tool Surface2526Seven tools in two tiers. All auto-resolve the target when `flake_uri` is27omitted and echo back what they resolved and ran (`resolved_target` and28`command`, or `commands` for `check("lint")`).2930Operational core:31- `build(flake_uri?, mode?)`: build the closure, no activation. A failed32 build carries `failed_derivation`.33- `diff(flake_uri?, mode?)`: what a switch would change (adds/removes/34 version bumps), plus a structured `packages` object when it parses.35 Include this in the reply and switch unless the user asked only to36 preview or check.37- `switch(flake_uri?, mode?, validate?, full_log?)`: activate. Records38 `rollback_generation`, returns a `summary` (units changed, derivations39 built, `packages` vs the rollback generation, `health`), and trims the40 log to a tail on success. `validate=True` gates on `check("dry-build")`.41 Remote refs and pin mismatches exit as `remote_ref_rejected` /42 `target_locked` before sudo or dry-build. `.` / `./` resolve to the43 pin when `$NIX_AGENT_FLAKE` is set. Newly failed units after44 activation are `status: "degraded"`; report `summary.health` and roll45 back unless the user asked to leave those units failed. Privileged46 argv uses `sudo -n`.47- `generations(action="list"|"rollback", mode?, generation?)`: list or48 roll back. NixOS list entries include `path` when the profile link49 exists. After `switch`, call50 `generations(action="rollback", generation=<rollback_generation or id>)`.51 Bare `generations(action="rollback")` is previous-generation only.52 Unknown ids return `unknown_generation` and run no command.5354Config introspection:55- `eval_config(attr, flake_uri?, mode?)`: final merged value of any56 config attribute on THIS machine, after all modules/overlays.57 `mcp-nixos` says what an option means; this says what it is. Pass a58 **list** for `attr` to evaluate many in one call (per-attr `results`).59 All attrs ok or mixed → `ok` (failures in `results`); every attr60 failed → `failed` with `first_error` from the first failed entry that61 has one. Values above ~2 KB degrade to attr names / length / a head62 slice, marked `truncated: true`. A missing hostname attr falls back to63 a unique flake host or returns `unknown_host` with `hosts`. In NixOS64 mode, a missing option is retried as `home-manager.users.<user>.<attr>`65 (`hm_rewritten: true`).66- `locate_option(attr, flake_uri?, mode?)`: which file sets an option,67 as `declarations` and `definitions` (`{file, value}` per file). Earns68 its slot as that which-file answer, not as a cap (measured69 `environment.systemPackages` is 24 KB → 20 KB). Use this instead of70 grepping the tree. `status` is `not_an_option` for plain config values71 (use `eval_config` there). For integrated HM, spell the attr72 `home-manager.users.<user>.<attr>` with `mode="nixos"`; if you pass73 the unprefixed attr, the tool retries that spelling (`hm_rewritten`).74 Missing hostname → `unknown_host` with `hosts`.75- `check(level, flake_uri?, mode?)`: validation ladder, fast to slow:76 `"lint"` (statix + deadnix, structured `findings`), `"dry-build"`,77 `"dry-activate"` (NixOS only). Dry-activate uses the same remote/pin78 classifier as `switch` and attaches `privilege` on sudo auth failure.7980Formatting is not a tool: format edited files with the flake's own81formatter (`nix fmt`, or `nixfmt` on the files) via your Bash tool. Repo82onboarding is the `nix-agent inspect-flake` CLI subcommand, not a tool; the83`nix-agent-init` skill drives it.8485## Picking `mode` (read before any HM change)8687`mode` defaults to `"nixos"`. Do NOT reflexively switch to88`"home-manager"` just because the task touches Home Manager options.89That is the most common way to operate on the wrong config.9091- **Integrated HM** (wired in as a NixOS module via92 `home-manager.nixosModules.home-manager` + `home-manager.users.*`):93 no separate `home-manager switch`. HM is built and activated as part94 of the system closure, so use `mode="nixos"` (the default) and95 `switch` the whole system. The common laptop/desktop layout.96- **Standalone HM** (its own flake exposing `homeConfigurations.*`,97 applied with `home-manager switch`): use `mode="home-manager"`.9899If both a NixOS flake and a standalone `~/.config/home-manager` flake100exist on the machine, the standalone one is often vestigial. Confirm101which is actually active (`eval_config` against each, or check what the102running generation was built from) before mutating. When in doubt,103`mode="nixos"` is the safer guess.104105For nonstandard or multi-flake layouts, do not rely on auto-resolution:106set `NIX_AGENT_FLAKE` once (or `NIX_AGENT_HM_FLAKE` for standalone HM,107which falls back to `NIX_AGENT_FLAKE`), or pass an explicit `flake_uri`108like `/home/you/nixos#host`. Either pins the target exactly.109110**Wrong-host symptom:** `unknown_host` lists `hosts` from the flake. A111`failed` envelope whose `first_error` names a missing112`nixosConfigurations."<hostname>"` means auto-resolution could not113recover; pass an explicit `flake_uri` (`.../repo#realhost`) or114`$NIX_AGENT_FLAKE`, not by retrying the same hostname.115116## Workflow1171181. Discovery (if needed): query `mcp-nixos` for packages/options;119 `eval_config` for what the machine currently resolves; `locate_option`120 for which file to open.1212. Edit `.nix` files with your native file tools.1223. Format with the flake's formatter (`nix fmt`, or `nixfmt` on the edited files) via Bash, then `check("lint")`: fix findings worth fixing.1234. `check("dry-build")`: catches eval/build errors cheaply.1245. `diff()`: include the changeset in the reply and switch unless the125 user asked only to preview or check.1266. `switch()`: report the result and `rollback_generation`. Keep that127 value. `status: "degraded"` means activation succeeded but units128 newly failed — report `summary.health` and roll back unless the user129 asked to leave those units failed.1307. On failure at any step: read `first_error`, then `error_detail`, then131 `failed_derivation.log_tail`; fix and retry. `status: "preflight_failed"`132 means `switch(validate=True)` never activated — fix the nested133 `preflight` dry-build, do not retry activation. A `privilege` field134 means sudo auth failed, not a Nix error. `unknown_generation`,135 `unknown_host`, `target_locked`, and `remote_ref_rejected` are early136 exits: no command ran. On regret after a switch:137 `generations(action="rollback", generation=<rollback_generation or id>)`.138 Bare `generations(action="rollback")` is previous-generation only,139 and is only the right default when nothing else has switched since.140141Steps 3 through 5 are judgment calls, not gates. For a trivial change,142going straight to `switch` is fine.143144## Token discipline: the envelope is the interface145146This server pre-digests Nix's firehose. An agent that re-runs `nix log`147or `systemctl` after every operation throws that away. Read the fields;148do not re-fetch.149150- **On failure, read three fields in order and stop.** `first_error` is151 the actionable line. `error_detail` is `{message, file, line, column,152 trace}` when Nix emitted an eval error: a direct file:line:column edit153 target. `failed_derivation.log_tail` is the failing builder's log,154 already fetched. Do NOT run `nix log` or re-run with `full_log=True`155 unless these fields are absent.156- **After a switch, read `summary`, do not re-probe.** `status: "degraded"`157 means units `newly_failed`; treat that as a problem, not success, and158 roll back with159 `generations(action="rollback", generation=<rollback_generation>)`160 unless the user asked to leave those units failed.161 `summary.health` reports units `newly_failed`/`resolved`/`still_failed` with journal162 tails for the first five newly failed units; `summary.packages` reports changes163 vs the rollback generation. These replace running `systemctl --failed`164 or a second `diff()`.165- **Batch attr checks.** `eval_config([...])` answers N questions in one166 call. Mixed results stay `ok` with failures in `results`; if every167 attr failed, top-level `status` is `failed` with `first_error` from168 the first failed entry that has one. A `truncated: true` value means169 eval a child attr for the part you need, NOT retry for full output.170- **`locate_option` before grepping.** It answers "which file sets this"171 in one call; a tree-wide grep does not. That is why it has a slot, not172 because it caps a firehose (24 KB → 20 KB on173 `environment.systemPackages`).174- **Byte accounting is off the envelope.** `raw_bytes` / `returned_bytes`175 live in the usage log (`NIX_AGENT_USAGE_LOG=1`, then `nix-agent usage`),176 not on the tool response. Do not look for them in MCP results.177- **Escape hatches are deliberate last resorts.** `full_log=True` and the178 raw `output` field exist for the rare case the trimmed view genuinely179 lacks what you need; reaching for them by default defeats the server.180181## Onboarding a repo182183First time in an unfamiliar config? Run `/nix-agent-init`: it runs184`nix-agent inspect-flake` once and generates `AGENT_MAP.md`, `CLAUDE.md`185(+ an `AGENTS.md` symlink), and `.mcp.json` from the observed facts, never186boilerplate.187188## Hard Rules189190- Never write secret payloads into config files; reference secrets via191 sops-nix/agenix and only edit references.192- Never call `switch` when the user asked only to check or preview;193 `diff` is the preview. Otherwise the default is apply: switch after194 `diff()` without waiting.195- `status: "degraded"` means activation succeeded but units newly196 failed; report `summary.health` and roll back unless the user asked197 to leave those units failed.198- Host allowlists cannot see `flake_uri`. Privileged tools reject remote199 refs and honor `$NIX_AGENT_FLAKE` / `$NIX_AGENT_HM_FLAKE` as an200 anti-footgun (the HM lock does not fall back to the NixOS pin). Do not201 treat the pin as a security boundary. Sudoers must be narrowed to that202 directory. Default install is unprompted activation plus passwordless203 sudo for this flake. Do not re-ask for confirmation. Lower trust204 (host prompts, sudo password) only if the user asked.205- After `switch`, undo with206 `generations(action="rollback", generation=<rollback_generation>)`.207 Bare rollback is previous-only.