A TUI to browse and diff NixOS configurations at the config-level
Rust
137
21 commits
updated Feb 22, 2026
WARNING: This is an experimental proof-of-concept. The patched Nix evaluator may have subtle bugs, please do NOT use this in production without validating what it does. All internal NixOS tests pass, but hidden invariants may be broken. (tho it does work, just no guarantees!)
A TUI which allows you to browse values and dependencies of NixOS options that influenced a NixOS configuration. This can be used to diff two configurations at the configuration/option-level as opposed to diffing the resulting derivation.
For a detailed explanation and showcase, see the blog post or the NixOS Discourse post.
|
Dependency graph (graphia, not in the TUI)
|
Exploring a configuration
|
|
Diffing two configurations
|
Textual diff
|
You do not need to change your system's Nix daemon. Since all changes are in expression evaluation, it suffices to run the patched Nix CLI.
Enter a shell with the patched nix binary and the nixos-config utility:
nix shell github:oddlama/nix/thunk-origins-v1 github:oddlama/nixpkgs/thunk-origins-v1#nixos-config
Define a host using the patched nixpkgs and set trackDependencies = true:
# flake.nix
{
inputs.nixpkgs.url = "github:oddlama/nixpkgs/thunk-origins-v1";
outputs = { self, nixpkgs }: {
nixosConfigurations.host1 = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
trackDependencies = true;
modules = [{
boot.loader.grub.device = "nodev";
fileSystems."/" = {
device = "/dev/sda1";
fsType = "ext4";
};
system.stateVersion = "25.11";
}];
};
};
}
Clone https://github.com/oddlama/nixpkgs, check out thunk-origins-v1, then:
# toplevel.nix
let
nixpkgs = import ./nixpkgs {};
lib = nixpkgs.lib;
in import ./nixpkgs/nixos/lib/eval-config.nix {
inherit lib;
trackDependencies = true;
modules = [{
boot.loader.grub.device = "nodev";
fileSystems."/" = {
device = "/dev/sda1";
fsType = "ext4";
};
system.stateVersion = "25.11";
}];
}
Build the tracked configuration (expect ~10-20s extra evaluation time):
nix build --print-out-paths .#nixosConfigurations.host1.config.system.build.toplevel
The resulting toplevel will contain tracking.json, tracking-explicit.json,
and tracking-deps.json alongside the usual system files.
Explore, show or diff:
# Explore a built toplevel
nixos-config show /nix/store/...-nixos-toplevel-tracked
# Explore from flake reference (no build needed)
nixos-config show .#host1
# Diff two toplevels
nixos-config diff /nix/store/OLD /nix/store/NEW
# Diff showing only explicitly defined values
nixos-config diff --explicit /nix/store/OLD /nix/store/NEW
# Textual diff as pseudo configuration.nix
nixos-config text-diff --explicit /nix/store/OLD /nix/store/NEW
A patch for the Nix evaluator adds primitives to create tracking scopes, tag
thunks with origin paths, and record attribute accesses. A small integration
in lib/modules.nix and eval-config.nix uses these primitives to tag all
option value thunks and register the config/options attrsets for tracking.
When any option value is forced during evaluation, its origin path is pushed as the "current accessor" context. Any accesses to tracked attrsets during that evaluation are recorded as dependencies. After evaluation, all edges and config values are serialized into the toplevel output.
For a detailed technical explanation, see the blog post.
The patches for Nix and nixpkgs are available in contrib/:
| Patch | Description |
|---|---|
contrib/nix-add-thunk-origins.diff | tracking builtins |
contrib/nixpkgs-add-tracking.diff | evalModules integration, dependency-tracking.nix post-processing |
They are also maintained as branches:
oddlama/nix@thunk-origins-v1oddlama/nixpkgs@thunk-origins-v1If you want to work with the tracking data directly instead of using
nixos-config, all information is available on the evalModules result:
let
nixos = nixpkgs.lib.nixosSystem {
trackDependencies = true;
modules = [ ./configuration.nix ];
};
# Force evaluation of toplevel first to record all dependencies
dependencyTracking = builtins.seq nixos.config.system.build.toplevel nixos.dependencyTracking;
in {
toplevel = nixos.config.system.build.toplevel;
inherit (dependencyTracking)
rawDeps # all raw dependency edges
filteredDeps # filtered + transitive closure
configValues # all leaf config values (JSON-safe)
explicitConfigValues # only explicitly defined value (JSON-safe)
leafNodes # leaf node paths
keptNodes # all kept node paths
counts # summary statistics
rawDotOutput # Graphviz DOT of raw deps
filteredDotOutput; # Graphviz DOT of filtered deps
}
Licensed under either of
at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
21 commits
Rust
99.1%
A TUI to browse and diff NixOS configurations at the config-level
Rust
137
21 commits
updated Feb 22, 2026
WARNING: This is an experimental proof-of-concept. The patched Nix evaluator may have subtle bugs, please do NOT use this in production without validating what it does. All internal NixOS tests pass, but hidden invariants may be broken. (tho it does work, just no guarantees!)
A TUI which allows you to browse values and dependencies of NixOS options that influenced a NixOS configuration. This can be used to diff two configurations at the configuration/option-level as opposed to diffing the resulting derivation.
For a detailed explanation and showcase, see the blog post or the NixOS Discourse post.
|
Dependency graph (graphia, not in the TUI)
|
Exploring a configuration
|
|
Diffing two configurations
|
Textual diff
|
You do not need to change your system's Nix daemon. Since all changes are in expression evaluation, it suffices to run the patched Nix CLI.
Enter a shell with the patched nix binary and the nixos-config utility:
nix shell github:oddlama/nix/thunk-origins-v1 github:oddlama/nixpkgs/thunk-origins-v1#nixos-config
Define a host using the patched nixpkgs and set trackDependencies = true:
# flake.nix
{
inputs.nixpkgs.url = "github:oddlama/nixpkgs/thunk-origins-v1";
outputs = { self, nixpkgs }: {
nixosConfigurations.host1 = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
trackDependencies = true;
modules = [{
boot.loader.grub.device = "nodev";
fileSystems."/" = {
device = "/dev/sda1";
fsType = "ext4";
};
system.stateVersion = "25.11";
}];
};
};
}
Clone https://github.com/oddlama/nixpkgs, check out thunk-origins-v1, then:
# toplevel.nix
let
nixpkgs = import ./nixpkgs {};
lib = nixpkgs.lib;
in import ./nixpkgs/nixos/lib/eval-config.nix {
inherit lib;
trackDependencies = true;
modules = [{
boot.loader.grub.device = "nodev";
fileSystems."/" = {
device = "/dev/sda1";
fsType = "ext4";
};
system.stateVersion = "25.11";
}];
}
Build the tracked configuration (expect ~10-20s extra evaluation time):
nix build --print-out-paths .#nixosConfigurations.host1.config.system.build.toplevel
The resulting toplevel will contain tracking.json, tracking-explicit.json,
and tracking-deps.json alongside the usual system files.
Explore, show or diff:
# Explore a built toplevel
nixos-config show /nix/store/...-nixos-toplevel-tracked
# Explore from flake reference (no build needed)
nixos-config show .#host1
# Diff two toplevels
nixos-config diff /nix/store/OLD /nix/store/NEW
# Diff showing only explicitly defined values
nixos-config diff --explicit /nix/store/OLD /nix/store/NEW
# Textual diff as pseudo configuration.nix
nixos-config text-diff --explicit /nix/store/OLD /nix/store/NEW
A patch for the Nix evaluator adds primitives to create tracking scopes, tag
thunks with origin paths, and record attribute accesses. A small integration
in lib/modules.nix and eval-config.nix uses these primitives to tag all
option value thunks and register the config/options attrsets for tracking.
When any option value is forced during evaluation, its origin path is pushed as the "current accessor" context. Any accesses to tracked attrsets during that evaluation are recorded as dependencies. After evaluation, all edges and config values are serialized into the toplevel output.
For a detailed technical explanation, see the blog post.
The patches for Nix and nixpkgs are available in contrib/:
| Patch | Description |
|---|---|
contrib/nix-add-thunk-origins.diff | tracking builtins |
contrib/nixpkgs-add-tracking.diff | evalModules integration, dependency-tracking.nix post-processing |
They are also maintained as branches:
oddlama/nix@thunk-origins-v1oddlama/nixpkgs@thunk-origins-v1If you want to work with the tracking data directly instead of using
nixos-config, all information is available on the evalModules result:
let
nixos = nixpkgs.lib.nixosSystem {
trackDependencies = true;
modules = [ ./configuration.nix ];
};
# Force evaluation of toplevel first to record all dependencies
dependencyTracking = builtins.seq nixos.config.system.build.toplevel nixos.dependencyTracking;
in {
toplevel = nixos.config.system.build.toplevel;
inherit (dependencyTracking)
rawDeps # all raw dependency edges
filteredDeps # filtered + transitive closure
configValues # all leaf config values (JSON-safe)
explicitConfigValues # only explicitly defined value (JSON-safe)
leafNodes # leaf node paths
keptNodes # all kept node paths
counts # summary statistics
rawDotOutput # Graphviz DOT of raw deps
filteredDotOutput; # Graphviz DOT of filtered deps
}
Licensed under either of
at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
21 commits
Rust
99.1%