There is a recent blog post about Nix Flakes vs Guix: https://coopi.neocities.org/posts/nix-flakes-vs-guix

After briefly skimming it, I realized it definitely not fact checked. After re-reading it again I started to suspect that the text of the post was highly LLM assisted.

I wanted to ignore it at first, but later I saw Ludovic endorsing it and a few other peers keep sharing the post with me, so I decided to write a review.

Despite the way that post is written (with LLMs or not), it still touches the important topics. And I've spent quite some time working and thinking on them, so here I'm to share my thoughts.

I'll replicate the sections structure and will be writing my notes and quote original text when necessary.

P.S. When I was writing this text, I found my old notes on the related topic: Guix Channels are not Flakes.

Table of Content

There is a TOC of a blog post under review:

> 1. Who is this for?
> 2. What even is a flake, anyway?
> 3. Declaring dependencies: inputs vs. channels
>
>     3.1. Flakes: inputs
>     3.2. Guix: channels
>     3.3. The comparison
>
> 4. Pinning dependencies: flake.lock vs. guix describe
>
>     4.1. Flakes: flake.lock
>     4.2. Guix: guix describe and guix time-machine
>     4.3. The comparison
>
> 5. Purity: enforced isolation
>
>     5.1. Flakes: pure evaluation mode
>     5.2. Guix: purity by design
>     5.3. The comparison
>
> 6. The output schema: what your project produces
>
>     6.1. Flakes: structured outputs
>     6.2. Guix: first-class records and modules
>     6.3. The comparison
>
> 7. Development environments: devShells vs. manifests
>
>     7.1. Flakes: devShells
>     7.2. Guix: guix shell and manifests
>     7.3. The comparison
>
> 8. System configuration: nixosConfigurations vs. operating-system
>
>     8.1. Flakes: nixosConfigurations
>     8.2. Guix: operating-system
>     8.3. The comparison
>
> 9. So what does Guix NOT have?
>
>     9.1. A standard project entry point
>     9.2. A registry and quick-install syntax
>     9.3. nix flake show
>
> 10. And what does Guix have that flakes don't?
>
>     10.1. guix time-machine
>     10.2. Grafting
>     10.3. First-class package records
>     10.4. Full-source bootstrapping
>     10.5. Channel authentication
>
> 11. Summary table
> 12. So… who wins?
> 13. Yes I actually cited my sources :3

"2. What even is a flake, anyway?"

This is the longest note, feel free to skip if you familiar with overall nix and flakes history.

There was an original Nix with it not-a-perfect bunch of CLI tools: nix-channel, nix-shell, nix-build, nixos-rebuild, etc, etc.

They did the job, but were far from perfect. For example they used NIX_PATH for finding files with nix expressions. Nix lang itself allows to access environment variables and local fs (which is obviously bad for reproducibility, as well for maintainability and other properties of the code). AFAIR, CLI tools themselves were not very consistent.

Even on top of those tools people started to make their projects. To make those projects reproducible (so other person can clone and run/build it) they needed some way to pin the dependencies (as you can't be sure that nix channels in your system are exactly the same as of yours colleague). And number of tools appeared: from simple fetchTarball of the exact nixpkgs revision, to more advanced nix-pin, niv, npins and probably a dozen more of alternatives and ad-hoc solutions.

Each of them had it own downsides, and most of them were incompatible and noninteroperable with each other and often not even composable with themselves.

So, what is Nix flakes? Nix flakes is an attempt to provide a standardized format for writing nix expressions, with explicit inputs and outputs. + a set of tools for managing/pinning all of that.

Nix flakes can be composed (reference another flake and reuse it outputs and/or override their inputs). They can be hermetically evaluated (in pure enironment, without accessing env vars, local fs, etc). Pay attention that nix evaluation and package build is two different phases: packages themselves are already built in isolated environment by build daemon, but to get "build recipes" you need to evaluate a nix expression and its better to be hermetic as well.

To sum up flakes supposed to provide:

  1. Unified Project/Repo/Nix expression Structure
  2. Explorability
  3. Composability
  4. Dependency resolution, overriding and pinning
  5. Hermetic evaluation
  6. Convinience
> Now here's the key insight: Guix already had solutions for most of
> these before flakes were introduced in Nix 2.4 on November 1, 2021
> (Project, 2021). The channels mechanism landed in Guix around
> 2018–2019 (Contributors, 2025a). And the solutions are orthogonal —
> you can use each one independently, without buying into a single
> monolithic abstraction.

Almost none of the items above are covered by Guix yet. And some of them are technically impossible without fundamental architecture changes. Let's go through other sections and explore each point in more details.

"3 Declaring dependencies: inputs vs. channels"

> Running guix pull fetches all channels, compiles them, and makes
> their modules available to every guix command. This is your
> dependency resolution step.
>
> Channels can declare dependencies on other channels using a
> .guix-channel file in the repo root (Contributors, 2025b):
>
> ;; .guix-channel — lives at the root of a channel repository.  This tells Guix
> ;; that this channel depends on another channel called 'nonguix', so guix pull
> ;; will fetch both together.
> (channel
>   (version 0)
>   (dependencies
>     (channel
>       (name 'nonguix)
>       (url "https://gitlab.com/nonguix/nonguix"))))
>
> This is roughly analogous to inputs in a flake — one channel can
> pull in another. When you guix pull, all transitive channel
> dependencies are fetched together.

There is a huge difference in dependency resolution algorithm.

Imagine in your flake you have flakes A and B as inputs. A depends on flake C@rev1, B depends on flake C@rev2. (Revisions are always pinned, when flake.lock is present).

You will have 4 items resolved A@some-rev1, B@some-other-rev, C@rev1 and C@rev2. The nix expressions from flake A will use code from C@rev1 and B from C@rev2.

If you want to override inputs for flake A, to use the same version of C as in flake B, you can do it. If you want build A against C@rev3 you can do it as well. Just specify input override for the inputs.A.

Now to the Guix channels. Imagine similiar situation. Channel A depends on channel C@rev1, channel B on C@rev2 and your channel depends on both A and B.

During the resolution you will end up with only 3 channels (A@latest-revA, B@latest-revB, and C@rev1). And B will be built against C@rev1 and very likely will fail in general case.

If for some reason A specified C as a dependency, but did not specified the revision, you will end up with (A@latest-revA, B@latest-revB, and C@rev2).

There is a reason for it. As channels use guile modules, you will get name clash if you have two revisions of the same channel. In fact you can get module clashes anyway (same module name comming from different channels) and then the first loaded will win (you will never know which one, haha).

P.S. There are inferiors to be able to have multiple revisions of the same channel, but they are hacky, unreliable and not used in practice.

To sum up:

  1. (minor) You can git unpinned transitive dependency and it will be your work to pin and manage them.
  2. In guix you can't have channels with the same name, but different revisions. So you forced to build all the channels against one version of the dependency. Very ironic for a functional package manager.
>  Both systems let you declare external dependencies and pull them in
>  automatically. The main differences:
>
>     - Flakes are per-project — each repo has its own flake.nix with
>       its own inputs. Channels are system-wide or per-user — your
>       channels.scm applies to all guix invocations. This means flakes
>       naturally support different projects with different dependency
>       sets, while with Guix, you'd typically use guix time-machine or
>       separate profiles to achieve the same effect.
>
>     - Flakes use a URL-like syntax for references
>       (github:NixOS/nixpkgs, git+https://...) while channels use
>       plain Git URLs. The flake syntax is more ergonomic for quick
>       references, but channels are simpler and more explicit.
>
>     - Flakes support non-flake inputs (flake = false;) for repos
>       that don't contain a flake.nix. In Guix, a channel is just a
>       Git repo with Scheme files — there's no special opt-in
>       required. Any repo with Guile modules can be a channel.
>

There is no need to compare guix pull and flakes. nix-channel is what the closest to guix pull in this context. They are quite similiar. There is no per-user/per-project distinction. Both guix pull and nix-channel are "per-user" tools.

If we talk about ad-hoc guix time-machine + channels-lock.scm + .guix-channel. Then it's far from flakes, it's somewhere one step behind "nix-pin and friends times". One step behind because their is no widespread guix-pin tools yet, everyone is doing what they think is better. And if it careful enough it usually quite monstrous: 2024-02-10 Reproducible Dev Environment Workflow with Guix.

"4. Pinning dependencies: flake.lock vs. guix describe"

I don't want to nit-pick the whole section. So I'll go straight to important points and will ignore the rest.

> This is your lock file, essentially. It lives in
> ~/.config/guix/current (as a Guile profile), not as a file in your
> project directory.

It's a profile's provenance. Lock file should be created and available straight after dependency resolution, not after everything is already built. I would prefer to see a lock file immediately to know what I will be working with, not 3 hours after, when everything is already built.

> The guix time-machine command is genuinely unique and has no direct
> flake equivalent. It lets you travel to any point in Guix's history
> — not just to pinned dependency versions, but to a completely
> different state of the package collection (Contributors,
> 2025d). This is incredibly powerful for reproducibility. Like, you
> can run code from three years ago and it JUST WORKS?? That's wild!!

There is an equivalent of guix time-machine -C channel-lock.scm -- build ...

nix build --reference-lock-file alternative-flake.lock ...

If for some reason one need an exact version of nix cli, they can achieve it with the example above and run nix whatever .

> flake.lock is per-project and automatic. guix describe is per-user
> and automatic, while channels.scm with pinned commits is per-project
> but manual.

Yeah, there is no good alternative to flake-like mechanism in Guix and significantly affect UX/DX. And to mimic a fraction of flakes you need to write a hundred lines of code: 2024-02-10 Reproducible Dev Environment Workflow with Guix.

"5. Purity: enforced isolation"

> Guix doesn't need a "pure evaluation mode" because its evaluation is
> already pure by convention (LWN.net, 2024).

It's not clear what this link references to, there is no mention of pure evaluation mode and it's not clear what "pure by convention" means. I didn't find any related text or comments on LWN.

> Guile modules don't have access to environment variables unless you
> explicitly pass them in. There's no equivalent of $NIX_PATH — Guix
> resolves packages through its module system, not through a search
> path. builtins.currentSystem doesn't exist because there's no
> equivalent concept; you specify systems explicitly via package
> metadata and the --system flag.
>
> Guix achieves purity through architecture — Scheme modules are
> inherently more contained than Nix's channel/path system. Flakes
> achieve it through enforcement — a restricted evaluation mode
> layered on top of an otherwise impure system. Both get you to the
> same place. Guix's approach is arguably more elegant because it
> doesn't need to layer restrictions on top of something that was
> originally designed without them. (Though honestly, the fact that
> Nix managed to retrofit purity at all is kind of impressive — it's
> just a different philosophy of getting there.)
  1. Guix uses GUILE_LOAD_PATH for module resolution and loading.
  2. Guix CLI also respects GUIX_PACKAGE_PATH.
  3. Guile code has access to env variables, local fs, and all kind of side effects one can imagine. One can easly do (getenv "PHASE_OF_THE_MOON") from inside a package or operating-system definition.

All the claims about evaluation purity in this context are factually incorrect.

"6-8"

I skip those sections as less significant or because the points were already covered earlier. Many of the statements seems not fact-checked, but still they are too minor to spend time on them.

"9. So what does Guix NOT have?"

> 9.1. A standard project entry point
>
> Flakes have flake.nix — one file that declares dependencies, defines
> outputs, and provides a discoverable schema. There's nothing stopping
> you from finding flake.nix and understanding the project's structure
> at a glance.
>
> Guix projects are more convention-based. You might find manifest.scm,
> channels.scm, guix.scm, package.scm, or something else
> entirely. There's been some movement toward standardizing guix.scm as
> a project file that guix shell picks up automatically (Contributors,
> 2025h), but it's not as established as flake.nix.

Overall, it's true, but at this point I hope you already understand how far guix.scm from flake.nix is.

> 9.2. A registry and quick-install syntax
>
> nix build github:NixOS/nixpkgs#firefox
>
> Guix uses package specifications for similar ergonomics:
>
> guix shell hello
> guix install firefox
>
> But there's no equivalent of the registry for pointing at arbitrary
> Git repos by short name. You just use the URL. Honestly I think this
> is fine — the registry has been a source of confusion in the Nix
> world, since it's not always clear whether nixpkgs refers to the
> registry entry, a local path, or something else.

Recently guix introduced url for channels specification, so it's possible to do something like this:

guix time-machine -C https://my.org/prj/channel.scm -- shell my-package

or

guix time-machine -C 'swh:1:cnt:<content-hash-of-channels.scm>' -- shell my-package

There is are no registry for associating urls with short names in Guix, but I don't think it's any significant.

> 9.3. nix flake show
>
> The nix flake show command is genuinely nice — it gives you a tree
> view of everything a flake provides (Contributors, 2026). Guix has
> guix search for packages and guix system search for services, but
> there's no equivalent of "show me everything this project/repo
> provides." You just look at the Scheme files.

No flakes - no flake show, heh. There is no standardization, there is no expectations you can make. Composing, reusing? Nah.

10. And what does Guix have that flakes don't?

> I mentioned this earlier, but it deserves emphasis. The ability to
> say "run this command as if it were any arbitrary date in Guix's
> history" is incredibly powerful for reproducibility (Contributors,
> 2025d). Flakes can pin dependencies, but you can't easily say "run
> this with the version of nixpkgs from six months ago" without
> manually finding and specifying the commit. With Guix, guix
> time-machine --commit=... -- does exactly this. I love this feature
> SO MUCH!!

Already mentioned before, it's trivially achieveable on nix. With flakes and without.

> 10.2. Grafting
>
> Guix has a feature called grafting that lets it apply security
> updates to the dependency tree without rebuilding every dependent
> package (Contributors, 2025j). When a low-level library like glibc
> has a vulnerability, Guix can swap in the fixed version by rewriting
> store paths. Nix rebuilds everything. For a large dependency tree,
> the difference can be hours of build time. This is a HUGE advantage.

This is completely unrelated to flakes, it's about nix in general.

The lack of grafting in nix is true.

> 10.3. First-class package records
>
> In Nix, packages are functions — you call stdenv.mkDerivation {
> ... } and it returns a derivation, which is an opaque attribute
> set. In Guix, packages are <package> records — transparent data
> structures with named fields that you can inspect, transform, and
> compose with standard Scheme procedures (Contributors, 2025f).
>
> This means you can do things like:
>
> ;; package-input-rewriting walks the entire dependency graph and replaces every
> ;; occurrence of 'perl' with 'perl-minimal'.  Try doing that in one line with
> ;; Nix!!
> (package-input-rewriting `((,perl . ,perl-minimal)))
>
> ;; The 'inherit' keyword works like inheriting from a parent class — you get all
> ;; the fields of 'coreutils' but override just the ones you specify.
> (package
>   (inherit coreutils)
>   (arguments
>    (substitute-keyword-arguments (package-arguments coreutils)
>      ((#:tests? _ #f) #f))))
>
> Graph rewriting is trivial in Guix because packages are data, not
> functions ((rekado), 2019). Nix has overlays for a similar purpose,
> but they're less ergonomic because the opaque function interface
> makes inspection and transformation harder.
>

In Nix packages are values, and can be expected the same way as Scheme values. Package definition files are functions, which accept an attrset of everything (tools, pcakages, APIs), extract all necessary dependencies, construct and return a attrset value representing a derivation.

The cool thing here, Nix is lazily-evaluated and one can provide a different set of tools/packages to the same package definition to get a new package. That means if I update one package, all the dependent packages will be updated during evaluation.

There is a mechanism called overlays. Basically, it's just a function, which accepts takes argument a self-reference to new attrset and an old attrset. After that you express transformation as a simple function operating on recursive data structure.

Requires a bit of understanding to use it, but it is:

  1. convinient
  2. powerful
  3. first-class supported

In terms of simple vs easy. It's simple.

In Guix, you rewriting a graph is easy, you can get a transformer function, which will go and update all the affected packages. It's not lazy-evaluated, so it will update all the things right now, even if you will never use them.

The worst part, is that you need to get a set of packages to apply the input-rewrite transformer and when you define operating-system or home-environment you will have hard time injecting this rewrite into the right place. If you are not guix developer, it's almost impossible to do. Most likely, you will end up just duplicating part of the dependency graph and having multiple version of the same libs or will just get a conflicting version of the same binary in the profile and build failure.

Dependency rewriting in Guix is easy, but not simple.

In both nix and guix packages are first-class citizens, but package rewrite are first-class only in Nix. :'(

> 10.4. Full-source bootstrapping
>
> Guix is obsessive about bootstrapping from source (Contributors,
> 2025k). The entire system can be built from a tiny trusted computing
> base — a ~500-byte hex assembler, then the mes C compiler written in
> Scheme, then tcc, then the full GNU toolchain, and up from there
> ((janneke) Nieuwenhuizen, 2023). The bootstrappable builds project
> has the details and it is WILD. Nix relies on more binary
> seeds. This matters for trust and verifiability — if you can't audit
> the bootstrap chain, you can't truly verify that your system was
> built from the sources you think it was.

Nix is full source bootstrapped. However, multiple packages are not properly "packaged" and vendors dependencies and binary seeds.

> 10.5. Channel authentication
>
> Guix channels support cryptographic authentication out of the box
> (Contributors, 2025l). Each channel specifies an "introduction" — a
> specific commit and its Ed25519 signature — and Guix verifies the
> full chain of signatures from that introduction to the current
> commit. Flakes use HTTPS and GitHub's infrastructure for trust,
> which is a different and arguably less rigorous security model.

Yes, guix has commit authentication, nix doesn't. It's not necessary Ed25519, but arbitrary GPG keys, probably also SSH keys support will appear in the future.

Conclusion

Flakes are not perfect, they are stuck in "alpha" and unlikely to leave it, there is a further work and alternatives to flakes like Nixtamal. Still, Flakes showcased what is possible. They did a dependency pinning, input rewriting, hermetic evaluation and standardization. They made Nix UX/DX much saner.

Guix is not here, it's around "nix-pin times" at the moment. It requires a lot of fundamental work to get overlays and multiple revisions of the same channel. The good thing, that we don't need to repeat all the steps of the Nix and can learn from their experience and do better. But we have to be clear about current state and aknowledge our weaknesses.

Support

I've spent a half of my post-surgery vacation/recovery day on this write up. So if you find it useful, interesting or entertaining, don't hesitate to support me or my projects.