Nix Submodules Haven't Been Painful for a WhileWords: 4613

Context

Around a year and a half ago I wrote a post complaining about Nix and Git Submodules. That is no longer accurate and hasn’t been for a while. I’ve been kinda forgetting to write this despite a TODO item sitting in my backlog for quite a while.

Image showing how long a TODO item to create this very post has been sitting in my backlog

Yeah, it’s been 301 days since I made a note to write this. I’m lazy I guess.

Wait, so what changed?

Since February 4th, 2025, you can set input.self.submodules = true; in a Nix Flake and it will include your submodules as part of the Nix builds, fetches, etc.

It had a few bugs here and there for a month or two afterwards where it occasionally broke, but for a good while now it’s been rock solid. All of my complaints about needing to pass .?submodules=1 to the end of various Nix commands were rendered invalid since then.

An Example

So let’s say you have a Nix Flake in a Git repository that depends on submodules. For example, let’s say you use Agenix, Sops-Nix, or some other secrets management that stores those secrets in the repository. Currently, the recommendation is to just encrypt those secrets and leave the encrypted files publicly visible. I think that came straight from the bad idea fairy.

Let’s say we wanted to be a little more cautious and stop Harvest now, decrypt later strategies from working against us. A good strategy then, is to store the secrets outside of the public repository in a separate, private, Git repository and then pull those secrets in by adding them as a Git submodule to the public repository. I’ve done exactly that in my Nix config, see the secrets submodule — it’s a private submodule so random folks can’t just download my secrets and wait until a possible vulnerability crops up in Age and then harvest everything I have in there.

So, what I do now is simply add self.submodules to my Nix Flake inputs and use the secrets as a path input, over here. This works great! Public users can still browse my slop repository with all its sharp edges and terrible abstractions without being able to copy down even encrypted secrets.

Here's a snippet of that Flake if you don't want to browse:
{


  inputs = {
    nix.url = "git+https://github.com/nixos/nix?shallow=1";
    deploy-rs.url = "github:serokell/deploy-rs";
    nixos-facter-modules.url = "github:nix-community/nixos-facter-modules";
    nixos-hardware.url = "github:NixOS/nixos-hardware/master";
    nixpkgs.url = "git+https://github.com/NixOS/nixpkgs?shallow=1&ref=nixos-unstable";

    nixpkgs-unstable.url = "git+https://github.com/NixOS/nixpkgs?shallow=1&ref=nixpkgs-unstable";
    nixpkgs-stable.url = "git+https://github.com/NixOS/nixpkgs?shallow=1&ref=nixos-25.11";

    harmonia.url = "github:nix-community/harmonia";
    nix-post-build-hook-queue = {
      url = "github:newam/nix-post-build-hook-queue";
      inputs.nixpkgs.follows = "nixpkgs";
      inputs.treefmt.follows = "";
    };
    hyprland.url = "git+https://github.com/hyprwm/Hyprland?shallow=1";
    fenix = {
      url = "github:nix-community/fenix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    lanzaboote = {
      url = "github:nix-community/lanzaboote";
    };
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    agenix = {
      url = "github:yaxitech/ragenix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    impermanence.url = "github:nix-community/impermanence";
    disko = {
      url = "github:nix-community/disko";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    blog = {
      url = "git+https://git.pricehiller.com/price/Blog";
    };
    treefmt-nix.url = "github:numtide/treefmt-nix";
    apple-emoji-linux.url = "github:samuelngs/apple-emoji-linux";
    neovim-nightly-overlay = {
      url = "github:nix-community/neovim-nightly-overlay";
      inputs = {
        nixpkgs.follows = "nixpkgs";
      };
    };
    rofi-tools = {
      url = "github:szaffarano/rofi-tools";
      inputs = {
        nixpkgs.follows = "nixpkgs";
      };
    };
    zsh-completions = {
      url = "github:zsh-users/zsh-completions";
      flake = false;
    };
    oisd-blocklist = {
      url = "github:sjhgvr/oisd";
      flake = false;
    };
    copyparty = {
      url = "github:9001/copyparty";
      inputs = {
        nixpkgs.follows = "nixpkgs";
      };
    };
    nixcord.url = "github:kaylorben/nixcord";

    self.submodules = true;
    secrets = {
      url = ./secrets;
      inputs = {
        nixpkgs.follows = "nixpkgs";
        agenix.follows = "agenix";
        treefmt-nix.follows = "treefmt-nix";
      };
    };
  };

  outputs =
    inputs@{ self, nixpkgs, ... }:
    let
      inherit (self) outputs;
      forAllSystems =
        function:
        nixpkgs.lib.genAttrs
          [
            "aarch64-linux"
            "i686-linux"
            "x86_64-linux"
            "aarch64-darwin"
            "x86_64-darwin"
          ]
          (
            system:
            function (
              import nixpkgs {
                inherit system;
                overlays = [
                  inputs.agenix.overlays.default
                  self.overlays.modifications
                  self.overlays.additions
                ];
              }
            )
          );
      treefmtEval = forAllSystems (pkgs: inputs.treefmt-nix.lib.evalModule pkgs ./treefmt.nix);
    in
    {
      formatter = forAllSystems (
        pkgs: treefmtEval.${pkgs.stdenv.hostPlatform.system}.config.build.wrapper
      );
      packages = forAllSystems (
        pkgs:
        let
          bootstrapISO = self.nixosConfigurations.bootstrapper.config.system.build.isoImage;
        in
        {
          inherit bootstrapISO;
          default = bootstrapISO;
        }
      );
      overlays = import ./overlays { inherit inputs; };
      apps = forAllSystems (pkgs: {
        default = {
          type = "app";
          meta.description = "Run nixos-rebuild switch";
          program =
            pkgs.writeShellApplication {
              name = "nixos-rebuild-wrapper";
              runtimeInputs = with pkgs; [
                nix-output-monitor
                nixos-rebuild
                hostname
              ];
              text = ''
                MSG="Switching to NixOS configuration: '$(hostname)'"
                HEADER=$(printf "%''${#MSG}s\n" | tr ' ' "=")
                echo
                echo "$HEADER"
                echo "$MSG"
                echo "$HEADER"
                echo
                sudo nixos-rebuild switch --flake ".#$(hostname)" --accept-flake-config |& nom
              '';
            }
            |> pkgs.lib.getExe;
        };
        repl = {
          type = "app";
          meta.description = "Launch interactive repl to inspect config";
          program =
            pkgs.writeShellApplication {
              name = "inspect-flake";
              runtimeInputs = with pkgs; [
                git
              ];
              text = ''
                nix repl --file "$(git rev-parse --show-toplevel)"/repl.nix
              '';
            }
            |> pkgs.lib.getExe;
        };
      });
      devShells = forAllSystems (pkgs: {
        default = pkgs.mkShell {
          packages = with pkgs; [
            age
            agenix
            age-plugin-yubikey
            nixos-rebuild
            nixos-install-tools
            inputs.deploy-rs.packages.${pkgs.stdenv.hostPlatform.system}.deploy-rs
          ];
          shellHook = ''
            export RULES="$PWD/secrets/secrets.nix"
          '';
        };
      });
      checks = forAllSystems (pkgs: {
        formatting = treefmtEval.${pkgs.stdenv.hostPlatform.system}.config.build.check self;
      });
      nixosConfigurations =
        let
          clib = (import ./lib { lib = nixpkgs.lib; });
        in
        {
          orion =
            let
              hostname = "orion";
            in
            nixpkgs.lib.nixosSystem {
              specialArgs = {
                inherit self;
                inherit inputs;
                inherit outputs;
                inherit hostname;
                inherit clib;
              };
              modules =
                let
                  age-secrets = {
                    config = inputs.secrets.secrets.${hostname};
                  };
                in
                [
                  ./modules/nixos/base-programs.nix
                  ./modules/nixos/btrfs-rollback.nix
                  ./modules/nixos/grafana-alloy.nix
                  ./modules/nixos/vector.nix
                  ./modules/nixos/logviewer.nix
                  ./modules/nixos/persistence.nix
                  ./modules/nixos/dns
                  inputs.home-manager.nixosModules.home-manager
                  {
                    home-manager = {
                      sharedModules = [
                        inputs.agenix.homeManagerModules.default
                        inputs.nixcord.homeModules.nixcord
                        age-secrets
                        ./modules/hm/link-file.nix
                      ];
                      backupFileExtension = "hm.backup";
                      extraSpecialArgs = {
                        clib = (import ./lib { lib = nixpkgs.lib; });
                        inherit inputs;
                      };
                      useGlobalPkgs = true;
                      useUserPackages = true;
                      users.price = import ./users/price/home.nix;
                    };
                  }
                  inputs.nix-post-build-hook-queue.nixosModules.default
                  inputs.nixos-facter-modules.nixosModules.facter
                  inputs.nixos-hardware.nixosModules.dell-xps-15-9530
                  inputs.lanzaboote.nixosModules.lanzaboote
                  inputs.impermanence.nixosModules.impermanence
                  inputs.agenix.nixosModules.default
                  inputs.disko.nixosModules.disko
                  {
                    config = {
                      nixpkgs.overlays = [
                        inputs.neovim-nightly-overlay.overlays.default
                        inputs.nix-post-build-hook-queue.overlays.default
                        self.overlays.modifications
                        self.overlays.additions
                      ];
                    };
                  }
                  age-secrets
                  ./hosts/${hostname}
                ];
            };
          luna =
            let
              hostname = "luna";
            in
            nixpkgs.lib.nixosSystem {
              specialArgs = {
                inherit self;
                inherit inputs;
                inherit hostname;
                inherit nixpkgs;
                inherit clib;
              };
              modules = [
                ./modules/nixos/btrfs-rollback.nix
                ./modules/nixos/mail.nix
                ./modules/nixos/grafana-alloy.nix
                ./modules/nixos/openssh.nix
                ./modules/nixos/base-programs.nix
                ./modules/nixos/vector.nix
                ./modules/nixos/persistence.nix
                ./modules/nixos/dns
                inputs.nixos-facter-modules.nixosModules.facter
                inputs.impermanence.nixosModules.impermanence
                inputs.agenix.nixosModules.default
                inputs.disko.nixosModules.disko
                inputs.harmonia.nixosModules.harmonia
                inputs.copyparty.nixosModules.default
                {
                  config = inputs.secrets.secrets.${hostname};
                }
                ./hosts/${hostname}
              ];
            };
          bootstrapper =
            let
              hostname = "bootstrapper";
            in
            nixpkgs.lib.nixosSystem {
              specialArgs = {
                inherit self;
                inherit inputs;
                inherit hostname;
                inherit nixpkgs;
                inherit clib;
              };
              modules = [
                ./modules/nixos/openssh.nix
                ./modules/nixos/base-programs.nix
                {
                  config = {
                    nixpkgs.overlays = [
                      inputs.neovim-nightly-overlay.overlays.default
                    ];
                  };
                }
                ./hosts/${hostname}
              ];
            };
        };
      deploy.nodes =
        let
          deploy-rs = inputs.deploy-rs;
        in
        {
          luna = {
            hostname = "luna.hosts.pricehiller.com";
            fastConnection = true;
            profiles.system = {
              sshUser = "root";
              path = deploy-rs.lib.x86_64-linux.activate.nixos outputs.nixosConfigurations.luna;
            };
          };
        };
    };
}

If you’re keeping your encrypted secrets in a publicly visible repository, consider vendoring those out to an external Git repository and pulling ‘em back in as a Git submodule.

Anyhow, I can now cross that way overdue item off my TODO list.