How do I add boot menu entries to an install ISO?

I want to create a custom installation ISO that can be used for unattended installs. I want the boot menu to give you two options. The first option would do the unattended install and then immediately reboot. The second option would boot into a normal Bash prompt (so that you could rescue a system or debug the unattended install script).

Here’s what I’ve tried so far:

# 🅭🄍1.0 This file is dedicated to the public domain using the CC0 1.0 Universal
# Public Domain Dedication:
# <>

  pkgs = import <nixpkgs> { };
  configuration = { modulesPath, ... }: {
    imports = [ "${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix" ];
    specialisation.unattendedInstall = {
      inheritParentConfig = true;
      configuration = { pkgs, ... }: {
        # In the future, I’ll set systemd.defaultUnit to something that does an
        # unattended install, but for the sake of testing, I’m just installing
        # an additional package.
        environment.systemPackages = [ pkgs.hello ];
  nixOSPackage = pkgs.nixos configuration;

Building that expression does indeed produce an ISO, and when I boot that ISO, I can see that /run/current-system/specialisation/unattendedInstall/ does indeed exist, but there’s no GRUB entry for the unattendedInstall specialisation. How do I add boot menu entries for specialisations to an ISO? Should I be using something other than specialisations in order to accomplish what I’m trying to accomplish?

This is probably an issue with iso-image.nix since it manually specifies the boot entries and doesn’t use the grub module (which does add entries for specialisations).

OK, so I figured out how to do what I was trying to do, but it’s really overly complicated. Here’s a summary of how it works:

  1. Create a systemd service that does the unattended install (unattended-install.service).

  2. Create a systemd target that activates that service (

  3. Use an apply function to override the value of isoImage.contents.

  4. In the body of the apply function, locate a path that’s part of the eifDir package.

  5. Create a new customizedEfiDir package that copies all of the files from efiDir’s output.

  6. Make customizedEfiDir override the contents of ${efiDir}/EFI/boot/grub.cfg.

  7. Find one of the menu entry declarations in grub.cfg.

  8. Create a new menu entry that’s a copy of the first one.

  9. In the new menu entry, find where the Linux kernel command-line arguments are specified.

  10. Add this command-line argument:

  11. Write the newly generated menu entry to the bottom of grub.cfg.

  12. In the isoImage.contents list, replace the item that referred to efiDir with an item that refers to customizedEfiDir.

Now, there’s another package named eifImg that depends on the contents of the original efiDir package. I haven’t figured out how to override that package, but it doesn’t seem to matter.

Here’s the full code:

# 🅭🄍1.0 This file is dedicated to the public domain using the CC0 1.0 Universal
# Public Domain Dedication:
# <>

  configuration = { pkgs, lib, modulesPath, ... }: {
    imports = [ "${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix" ];
    config.systemd = {
      services.unattended-install = let
        dependencies = [ "" ];
      in {
        wants = dependencies;
        after = dependencies;
        description = "automatic NixOS installer";
        script = ''
          # This is where the actual unattended installation script would go.
          exit 1
        serviceConfig = {
          User = "nixos";
          Group = "users";
        unitConfig = {
          SuccessAction = "reboot";
          # This allows me to debug the unattended installation script if it
          # fails.
          OnFailure = "";
      targets.unattended-install = {
        wants = [ "unattended-install.service" ];
        description = "automatic installation environment";
    # This allows us to modify config.isoImage.contents after the rest of the
    # config has set it to something. See
    # <>
    options.isoImage.contents = lib.options.mkOption {
      apply = list: lib.lists.forEach list (item:
        if == "/EFI"
        then item // {
          source = let
            # I chose this name because the original package that I’m modifying
            # is called efi-directory:
            # <>
            pkgName = "efi-directory-customized";
            customizedEfiDir = pkgs.runCommand pkgName {
              nativeBuildInputs = [
            } ''
              set -e

              mkdir "$out"
              cp -r ${item.source} "$out/EFI"

              readonly grub_cfg="$out/EFI/boot/grub.cfg"
                echo -n "menuentry 'Unattended Install' "
                sift \
                  --multiline \
                  --regexp='--class installer\s.*?{.*?}\n' \
                  --only-matching \
                  --limit=1 \
              readonly existing_boot_entry
                # Thanks to Cyrus (
                # for this Ask Ubuntu comment:
                # <>
                echo -nE "$existing_boot_entry" \
                  | sed 's/linux.*/&'
              readonly new_boot_entry

              chmod +w "$grub_cfg"
              echo -nE "$new_boot_entry" >> "$grub_cfg"
              chmod -w "$grub_cfg"

              grub-script-check "$grub_cfg"
          in "${customizedEfiDir}/EFI";
        else item
  pkgs = import <nixpkgs> { };
  nixOSPackage = pkgs.nixos configuration;