Configuration

Every option on services.nixos-update-manager, and the flags behind them.

Everything is configured through services.nixos-update-manager. Only flakePath is required; the rest have defaults that work.

services.nixos-update-manager = {
  enable = true;
  flakePath = "${config.home.homeDirectory}/nixos";
  branch = "nixos-update";

  checkInterval = "6h";
  autoBuild = true;

  terminal = pkgs.alacritty;
  terminalArgs = [ "-e" ];
  editor = "nvim";
  claudePackage = pkgs.claude-code;

  icons = {
    error = "#f38ba8";
    updatesAvailable = "#f9e2af";
  };
};

Options#

OptionTypeDefaultPurpose
enableboolfalseRun the daemon as a user service bound to graphical-session.target.
flakePathstringrequiredGit checkout of the flake to update and merge back into.
branchstring"nixos-update"Branch the update is built on, in a worktree of its own. Fast-forwarded into main after a successful apply.
checkIntervalnull or spannullCheck on this interval unasked ("30m", "6h", "1h30m", "1d"). Null means only from the tray.
autoBuildboolfalseBuild as soon as a scheduled check finds an update. Applying is never automatic.
terminalnull or packagenullTerminal the troubleshooting entries open in. Null falls back to $TERMINAL; with neither, those entries are not offered.
terminalArgslist of string[ "-e" ]Arguments that make terminal run a command. [ ] for a terminal that takes it positionally.
editornull or stringnullEditor for the failure report, resolved from PATH. Null falls back to $EDITOR, then nvim.
claudePackagenull or packagenullClaude Code for “Troubleshoot with Claude”, passed by absolute path. Null resolves claude from PATH.
icons.<state>null or stringnull#rrggbb for one state’s icon. Null keeps the icon package’s own colour.
iconPackagepackagederivedThe rendered icon theme the daemon is pointed at. Defaults to the icons package with the non-null icons.* applied.
packagepackagethis flake’sThe daemon package.

The checkout and the branch#

flakePath must be a git checkout that declares nixosConfigurations.<hostname> and homeConfigurations."<user>@<hostname>". It is a path, not a flake reference: the daemon runs git in it.

The daemon never touches your working copy. It keeps a git worktree of the checkout on branch under ~/.cache/nixos-update-manager/worktree, runs nix flake update there, and builds from that. After an apply succeeds, the lock bump is fast-forwarded from branch into main, leaving the checkout as if you had updated and committed it yourself.

The consequence is the blocked state: any output from git status --porcelain --untracked-files=all in your checkout blocks checks, builds and applies, because the build would not see that work and the merge back would fight it. The daemon says so once, re-polls every 30 seconds, and clears itself when you commit.

Checking on a schedule#

checkInterval = "6h";
autoBuild = true;

checkInterval is a run of <number><unit> pairs — s, m, h, d — so "90s", "6h" and "1h30m" are all valid and zero is not. A check evaluates only. It puts nothing in the store, which is what makes an interval this short reasonable.

The scheduler lives inside the daemon rather than in a systemd timer, because it has to know daemon state: it never starts a check during a build or an apply, or while the checkout is blocked, and a manual check resets the clock. A scheduled check that finds nothing new is silent — the same main revision with the same flake.lock is the same update, so it notifies only on something genuinely new.

autoBuild extends that to the build: a scheduled check that finds an update goes straight on to building it, and you get the notification with the update already built. Applying is never automatic, with or without this.

Troubleshooting entries#

When a check, build or apply fails, the menu and the error notification grow Open failure report and Troubleshoot with Claude. Both write the same Markdown to ~/.cache/nixos-update-manager/troubleshoot.mdgit status, the branch revisions, the daemon’s own journal for this boot, the error chain — and open it in a terminal. These four options are what they open it with:

terminal = pkgs.alacritty;
terminalArgs = [ "-e" ];        # [ "-e" ] suits most; [ ] for kitty-style
editor = "nvim";
claudePackage = pkgs.claude-code;

terminal is the one that gates the rest. With no terminal and no $TERMINAL in the environment, neither entry is shown at all, rather than shown and broken.

editor is deliberately a PATH-resolved string rather than a package: the report should open in whichever editor your home profile installs, not in a second copy from the store. claudePackage is the opposite — an absolute store path, so it does not depend on the unit’s PATH. The Claude entry opens an interactive session rooted in the flake checkout with the report already submitted as its first prompt.

Icons and colours#

The daemon ships its own icons and looks up no freedesktop icon names at all, so the tray survives a broken Qt or GTK icon theme. Every status icon is the same silhouette — an arrow landing on a baseline — and the state is carried by the arrowhead, the baseline and the colour.

Each state’s colour is an option. Null keeps the icon package’s default, so you can respell one without restating the palette:

icons = {
  idle = "#cdd6f4";
  checking = "#89b4fa";
  upToDate = "#a6e3a1";
  updatesAvailable = "#f9e2af";
  applying = "#89dceb";
  building = "#94e2d5";
  error = "#f38ba8";
  blocked = "#fab387";
  menu = "#cdd6f4";     # the menu glyphs, which take no meaning from hue
};

The hues are chosen so the temperature says whose turn it is: cool while the daemon is working, warm when it is yours.

Setting any of these re-renders the icons package, which is a runCommand around resvg — a recolour does not rebuild the Rust crate. That is also why the daemon takes the icon theme as a runtime path rather than baking it in.

iconPackage is the escape hatch, and it is the one that matters while the module is enabled: the unit always passes --icon-dir, so overriding nixos-update-manager-icons inside package only changes what the binary falls back to on its own. To render the icons yourself:

iconPackage = pkgs.nixos-update-manager-icons.override {
  error = "#ff0000";
  building = "#94e2d5";
};

Command line#

The module turns the options above into flags, but the daemon runs by hand too. Each flag’s environment-variable equivalent is what a unit file of your own would set.

nixos-update-manager --flake PATH [--branch NAME] [--host NAME] [--user NAME]
    [--cache-dir DIR] [--icon-dir DIR] [--terminal CMD] [--terminal-arg ARG]...
    [--editor CMD] [--claude PATH] [--review-dialog PATH]
    [--check-interval SPAN] [--auto-build]
FlagEnvironmentDefault
--flake PATHNIXOS_UPDATE_FLAKErequired
--host NAMENIXOS_UPDATE_HOSTthe machine’s hostname
--user NAMENIXOS_UPDATE_USER$USER
--branch NAMEnixos-update
--cache-dir DIR$XDG_CACHE_HOME/nixos-update-manager
--icon-dir DIRNIXOS_UPDATE_ICON_DIRset by the package’s wrapper
--terminal CMDNIXOS_UPDATE_TERMINAL$TERMINAL
--terminal-arg ARG-e, repeatable; --terminal-arg "" for none
--editor CMDNIXOS_UPDATE_EDITOR$EDITOR, then nvim
--claude PATHNIXOS_UPDATE_CLAUDEclaude from PATH
--review-dialog PATHNIXOS_UPDATE_REVIEW_DIALOGnixos-update-review beside the daemon
--check-interval SPANNIXOS_UPDATE_CHECK_INTERVALoff
--auto-buildNIXOS_UPDATE_AUTO_BUILDoff

--host and --user name the flake attributes to build: nixosConfigurations.<host> and homeConfigurations."<user>@<host>". Override them when the attribute names do not match the machine — a shared configuration built under one hostname, say.

Passing any --terminal-arg replaces the default rather than adding to it, so a terminal that takes its command positionally is spelled --terminal-arg ""; the empty argument is dropped rather than passed on. In the module that is terminalArgs = [ ].