-----BEGIN YUNFACHI WEBSITE-----

Separate development inputs into their own `dev` flake to avoid redundant dependencies - 2026-08-20
[back to posts] [edit]

-----BEGIN POST CONTENT-----

First of all, this post is aimed at flakes that provide external outputs, like `nixosModules` or `packages` — your private NixOS configuration DOES NOT need this.

For example, you have a project called `niri-nix`, which introduces Niri's NixOS module and Niri package. But besides nixpkgs, that flake's inputs also include `flake-parts`, `treefmt-nix`, `git-hooks.nix`, and so on. Nix flake inputs are NOT lazily fetched. This means that if you only use, for example, `nixosModules.default` from that flake, and `nixosModules.default` does not mention `treefmt-nix` or any of the other inputs at all, they will still be added to the user's flake lock as dependencies of your flake, and they will still be downloaded.

? But how can you prevent some inputs from being redundantly and meaninglessly fetched?
> Implement a separate flake for those inputs — I call it a `dev` flake. The idea is to keep development-only inputs outside of the flake that is consumed by users.

There are 2 ways to set up a `dev` flake:

1. Use the `dev` flake for both inputs and logic, while the main flake only needs to integrate the `dev` flake's outputs into itself.
2. Use the `dev` flake only for inputs, while the logic stays in the main flake.

Both of these methods will be discussed. But to quickly answer which is the better option: if your `dev` logic is huge, then choose the first option; otherwise, you are good to go with the second one.

Let's begin with the first option. First, initialize the `dev` flake in the `dev/` directory:

01# dev/flake.nix
02{
03  inputs = {
04    # By this "hack", you can access your root flake's inputs to deduplicate them
05    # in the `dev` flake too. It only works in a git repository.
06    # my-project.url = "path:../.";
07    # nixpkgs.follows = "my-project/nixpkgs";
08
09    # Nixpkgs is required for flake-parts `perSystem` to work.
10    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
11
12    # Remove this if flake-parts is already used in your root flake
13    # or if you want to implement the mechanism yourself.
14    flake-parts = {
15      url = "github:hercules-ci/flake-parts";
16      # inputs.nixpkgs-lib.follows = "nixpkgs";
17    };
18
19    # Add any inputs that you want here; `treefmt-nix` is used as an example.
20    treefmt-nix = {
21      url = "github:numtide/treefmt-nix";
22      # inputs.nixpkgs.follows = "nixpkgs";
23    };
24  };
25
26  # Logic will be written in a separate file.
27  outputs = _: { };
28}

Then generate the lock file by running this command in the `dev/` directory:

 $nix flake lock

? Can I do this without flake-parts?
> Yes, but you'll need to implement your own mechanism for separating the `dev` flake's logic into another file. This can be a bit tricky. You may want to consider the second option below instead.

Now we'll create a flake-parts module in it. Don't worry, flake-parts is not required in your root flake:

01# dev/config.nix
02# The content of this file is just an example. You can write anything you want here:
03# https://flake.parts/options/flake-parts.html
04{ inputs, ... }: {
05  systems = [ "x86_64-linux" ];
06
07  imports = [ inputs.treefmt-nix.flakeModule ];
08
09  perSystem = _: {
10    # https://flake.parts/options/treefmt-nix.html
11    treefmt = {
12      projectRootFile = "flake.nix";
13
14      programs = {
15        nixfmt.enable = true;
16        deadnix.enable = true;
17      };
18    };
19  };
20}

Finally, you have to export the `dev` outputs to your root flake.
If you already use `flake-parts` in your root flake, you should use its partitions feature instead.
Otherwise, to avoid adding flake-parts to the root flake's dependencies, write this:

01# flake.nix
02{
03  inputs = {
04    # You may remove this; read the comments below.
05    flake-compat.url = "github:NixOS/flake-compat";
06
07    # ...
08  };
09
10  outputs = { flake-compat, self, ... }: let
11    # If you have `flake-compat` in your inputs, keep this:
12    devFlake = (import flake-compat { src = ./dev; }).defaultNix;
13
14    # But if you don't want `flake-compat` itself to become a dependency of your users, you can fetch the pinned flake-compat implementation directly:
15    # devFlake =
16    #   (import (builtins.fetchurl {
17    #     url = "https://raw.githubusercontent.com/NixOS/flake-compat/f275e157c50c3a9a682b4c9b4aa4db7a4cd3b5f2/default.nix";
18    #     sha256 = "sha256:0nycwx0777d451k63ghp6p5lcv791kziqgkvqzmr1qwzywkdk1cj";
19    #   }) { src = ./dev; }).defaultNix;
20
21    devOutputs = devFlake.inputs.flake-parts.lib.mkFlake {
22      inputs = devFlake.inputs // {
23        my-project = self;
24        self = devFlake;
25      };
26    } ./dev/config.nix;
27  in {
28    # Explicitly inherit outputs to prevent forcing evaluation of the `dev` flake
29    # when accessing root flake outputs.
30    inherit (devOutputs) checks formatter;
31
32    # Some output that is free from fetching `dev` inputs.
33    # nixosModules.default = ...;
34  };
35}

Harder than just having `treefmt-nix` in the root flake's inputs? Yes. But better for users? Absolutely.

This approach is also used in the following projects:

- ://github.com/unnamed-systems/nix-secrets (without flake-parts in the root flake)
- ://github.com/NixOS/flake-compat (without flake-parts in the root flake)
- ://github.com/hercules-ci/flake-parts (with flake-parts in the root flake)

---

Now, let's discuss the second option. First, you need to initialize the `dev` flake:

01# dev/flake.nix
02{
03  inputs = {
04    # By this "hack", you can access your root flake and its inputs to
05    # deduplicate them in the `dev` flake too. It only works in a git repository.
06    # my-project.url = "path:../.";
07    # nixpkgs.follows = "my-project/nixpkgs";
08
09    # Add any inputs that you want here; `treefmt-nix` is used as an example.
10    treefmt-nix = {
11      url = "github:numtide/treefmt-nix";
12      # inputs.nixpkgs.follows = "nixpkgs";
13    };
14  };
15
16  # This flake is only for inputs; there is no logic here.
17  outputs = _: { };
18}

Then generate the lock file by running this command in the `dev/` directory:

 $nix flake lock

And, finally, you have to access the `dev` inputs from the root flake:

01# flake.nix
02{
03  inputs = {
04    # You may remove this; read the comments below.
05    flake-compat.url = "github:NixOS/flake-compat";
06
07    # ...
08  };
09
10  outputs = { flake-compat, ... }: let
11    # If you have `flake-compat` in your inputs, keep this:
12    devInputs = (import flake-compat { src = ./dev; }).defaultNix.inputs;
13
14    # But if you don't want `flake-compat` itself to become a dependency of your users, you can fetch the pinned flake-compat implementation directly:
15    # devInputs =
16    #   (import (builtins.fetchurl {
17    #     url = "https://raw.githubusercontent.com/NixOS/flake-compat/f275e157c50c3a9a682b4c9b4aa4db7a4cd3b5f2/default.nix";
18    #     sha256 = "sha256:0nycwx0777d451k63ghp6p5lcv791kziqgkvqzmr1qwzywkdk1cj";
19    #   }) { src = ./dev; }).defaultNix.inputs;
20  in {
21    # Some output that is free from fetching `dev` inputs.
22    # nixosModules.default = ...;
23
24    # Actually, read the `treefmt-nix` manual for the proper setup of it; this post is not about how to use `treefmt-nix`.
25    formatter = devInputs.treefmt-nix.formatter;
26  };
27}

-----END POST CONTENT-----

-----END YUNFACHI WEBSITE-----

Evaluated with Nix 2.34.7 on x86_64-linux
Revision 48fa63b (modified on 2026-10-22)