- Prerequisites
 - Scenario 1 - Finding a Library
 - Scenario 2 - Understanding Benefits with an Example
 - The Why & Conclusion
 - Bonus: Garbage Collection
 
In NixOS, packages installed on the system do not follow the standard Linux filesystem hierarchy. I see this nonstandard approach perplex newcomers who are not familiar with it in a practical way. Thankfully, Nix(OS) provides a set of utilities to traverse its special quirks, in the form of commands. This article focuses on such commands, the motivation behind using them, and how they relate to the Nix store. It also aims to show (partially) why it is designed this way by using concrete examples.
By the end of this, you will know how to effectively navigate the Nix store and understand the underlying principles of its package management system; you will learn the Nix store’s structure, the relationship between derivations and store paths, and how symlinks and profiles work to manage packages.
This is my first blog, so I will keep it concise to get feedback on the pacing, tone, and the content being covered. I hope it’s helpful 🙏
Prerequisites
Before I begin, a few infamous terms need to be thrown out of the way. A derivation is a build recipe for software. It consists of inputs and outputs. Outputs are what we will later discover to be directly connected to paths in the Nix store (store paths). Store paths are always directories, and they are one kind of object in the Nix store, or a store object. There are other store objects, and in fact derivations are a store object. Store paths are a byproduct of derivations, and they contain the programs that we use.
Normally, for a top-to-bottom approach like this you would start with something everyone is familiar with, such as software applications. Instead, I will take the converse approach to give further motivation for the concepts in this blog.
Scenario 1 - Finding a Library
For the first scenario, suppose you are a programmer and have installed a library like glm on your NixOS system, which is a mathematical library for C/C++. If you do not understand what that means, imagine code that exists on your system as a package that you cannot directly run. Since it is not an executable, you cannot simply cheat your way by reading a symlink in your path (though specialized environment variables exist). So unless we want to run find in /nix/store and wait until the heat death of the universe for it to finish, and then go through duplicate installations of the library to find the one we are looking for, we need to learn some Nix commands.
First, we need to enter a Nix REPL environment to resolve the package directly. REPL stands for Read-Eval-Print Loop, which is a powerful environment for testing various Nix expressions. We can access it with the primary nix command.
$ nix repl
nix-repl> pkgs = import <nixpkgs> {}
nix-repl> pkgs.glm
«derivation /nix/store/25rdysybbl5aangkgkdznc57xfihq2zk-glm-1.0.1.drv»
This article assumes a basic familiarity with the Nix language, so a crash course for it will not be interleaved. But as a side note, attributes in Nix are evaluated lazily, so we do not worry about the import taking a long time. This is due to the fact that lazy evaluation guarantees expressions are evaluated only when they are needed—not when they are defined.
The last command yields a path because the interpreter treats it as a special kind of attribute set that was introduced as a derivation. Indeed, we can confirm that by running another command in the environment.
nix-repl> lib = pkgs.lib
nix-repl> lib.attrsets.isDerivation pkgs.glm
true
From the penultimate set of commands, we infer that the derivation is located at /nix/store/25rdysybbl5aangkgkdznc57xfihq2zk-glm-1.0.1.drv. (Derivations always use the .drv file extension.)
Every derivation (.drv) has a set of outputs. When a derivation is realised (i.e. built), these become what we will find out to be store paths.
The next step is finding the actual store path where the library file (.a or .so) resides in. We can use the nix-store command to achieve this.
$ nix-store --query --outputs /nix/store/25rdysybbl5aangkgkdznc57xfihq2zk-glm-1.0.1.drv
/nix/store/gkahdgly8x8z8b6cvgab4gij0niajx7x-glm-1.0.1-doc
/nix/store/nlrq8s273x3kbm2w4g90pymx1ab9ww3p-glm-1.0.1
The latter is the one we are looking for since the former is documentation. This shows that store paths are blobs in the Nix store where different versions of the same package may exist. It is now trivial to list the lib directory, which reveals that the library we are searching for is there and is statically-linked.
$ ls /nix/store/nlrq8s273x3kbm2w4g90pymx1ab9ww3p-glm-1.0.1/lib
libglm.a
pkgconfig
For programmers, it should be completely unsurprising that it is not in some sort of path because it need not be resolved automatically. Later, we will see that this is not the case for the other type of libraries.
It is also possible to inspect the derivation itself in a human-readable JSON form with the nix command, which lists the mysterious outputs that were mentioned before:
$ nix derivation show /nix/store/25rdysybbl5aangkgkdznc57xfihq2zk-glm-1.0.1.drv
...lots of json output
Here is a snippet from the part of the JSON that specifically contains the outputs:
"outputs": {
  "doc": {
    "path": "/nix/store/gkahdgly8x8z8b6cvgab4gij0niajx7x-glm-1.0.1-doc"
  },
  "out": {
    "path": "/nix/store/nlrq8s273x3kbm2w4g90pymx1ab9ww3p-glm-1.0.1"
  }
}
It is not hard to deduce from this that outputs have a one-to-one correspondence with store paths. So that is what they create when you realise a derivation with:
$ nix-store --realize /nix/store/25rdysybbl5aangkgkdznc57xfihq2zk-glm-1.0.1.drv
Note that the .drv file that has produced the store path we are looking for is called the deriver. The inverse operation—finding a derivation from a store path—simply involves asking for the deriver.
$ nix-store --query --deriver /nix/store/nlrq8s273x3kbm2w4g90pymx1ab9ww3p-glm-1.0.1/
/nix/store/25rdysybbl5aangkgkdznc57xfihq2zk-glm-1.0.1.drv
Putting everything together, we can see the chain of symlinks in this concise graph:
But why is it stored this way? We have the initial motivation for researching it, so all that remains is reverse engineering the design choice.
Scenario 2 - Understanding Benefits with an Example
Now suppose you had a store path with an executable, say git. We can leverage the fact that it is in the path to track it down more easily and figure out more stuff about our system. First, we find where its reference is in the path.
$ which git
/home/user/.nix-profile/bin/git
This is part of the running user profile. By definition, profiles in NixOS are symlinks to specific generations. More precisely, there is one profile in every system, but there can be more than one generation. Generations are simply snapshots of profiles, which are pointers for a set of installed packages or system configuration.
In our case, you can see the symlink here:
$ ls -l /home/user/.nix-profile
lrwxrwxrwx 1 user users 44 Apr  5  2025 .nix-profile -> /home/user/.local/state/nix/profiles/profile
Programs in the bin directory of profiles are also symlinks, so using ls we can find its store path:
$ ls -l /home/user/.nix-profile/bin/git
lrwxrwxrwx 20 root root 62 Jan  1  1970 /home/user/.nix-profile/bin/git -> /nix/store/v2rxk9xkcxsas64wl7ds31al15cm2wqd-git-2.50.1/bin/git
(The command readlink -f <symlink> is generally better for fetching the real file of a symlink, but ls -l will be used for demonstrative purposes.)
Equivalently, NixOS has a system profile, which resides in /run/current-system; it is the same as /nix/var/nix/profiles/system, whose content follows this structure:
$ ls /run/current-system
activate
bin
dry-activate
extra-dependencies
init
initrd
kernel-modules
nixos-version
sw
systemd
append-initrd-secrets
boot.json
etc
firmware
init-interface-version
kernel
kernel-params
specialisation
system
The only difference one needs to understand at this stage of learning is that the packages you install with Home Manager will be in the user profile, and the ones you do not are in the system profile.
Furthermore, /run/current-system/sw (where sw is a shorthand for “software”) is where all of our executables are symlinked to. And unsurprisingly, these symlinks point to the store paths we discussed!
$ ls -l /run/current-system/sw
lrwxrwxrwx 11 root root 55 Jan  1  1970 /run/current-system/sw -> /nix/store/fbdm2v6r78w3n0a7f78pbnjdwpdwi12x-system-path
$ ls /nix/store/fbdm2v6r78w3n0a7f78pbnjdwpdwi12x-system-path
bin
etc
lib
sbin
share
An important interjection here is that, since glm is a static library, it is not located in the lib subdirectory; only shared library files (.so) are in it. Just like a path needs to exist for regular programs, it suffices to view this as an equivalent path existing exclusively for shared libraries, which are used by other programs.
Since Git is installed system-wide, you can probably guess that it is in the bin (binary) directory, which in turn is symlinked to the abovementioned store path.
> ls -l /nix/store/fbdm2v6r78w3n0a7f78pbnjdwpdwi12x-system-path/bin/git
lrwxrwxrwx 62 root root 65 Jan  1  1970 /nix/store/fbdm2v6r78w3n0a7f78pbnjdwpdwi12x-system-path/bin/git -> /nix/store/gz9a9vvx15cwznzw2h1gr4k7778bbgqk-firejail-wrap/bin/git
Wrapping this section up, you can see everything we have discovered from the system profile in this graph:
The Why & Conclusion
As we observed in the previous example, the profile is a chain of symlinks whose destinations are store paths in the Nix store. The fact that we can simply switch applications by modifying the symlinks means that, if we later want to revert to an older version of an arbitrary package, say because it broke, all we have to do is make the symlinks point to the old store paths! And that’s precisely what your NixOS system does—automatically manages profiles and the Nix “generations” that you see upon system initialization.
The store path has the version of the package to help distinguish between new and old versions, and the random letters that you see in front of its name (in this case, fbdm2v6r78w3n0a7f78pbnjdwpdwi12x) is called the store path hash. If two store paths in the Nix store were to have the same name and version, then this random string of letters that gets generated through a mathematical process ensures that they do not conflict. The combination of these three attributes makes up the store path, and this is why the Nix store is called content-addressed.
Obviously, if we modify an existing store path, that could break the link between the derivation, but also make future reverts unreliable. This is why you are not allowed to edit it. The property is called immutability.
Lastly, the reason derivations are the way they are is they orchestrate all of the store paths and provide them all the necessary dependencies. We saw the inner details of how they achieve this in the first example.
And this concludes our witchhunt! We have seen how all of these little things are interconnected in the ecosystem, and you have learned the fundamentals of this complex monolith called the Nix store. I leave the traversal of the user profile located in ~/.nix-profile as an exercise to the reader.
To summarize everything that I demonstrated, below is a cheatsheet of the commands and material introduced above.
- 
nix-store --query --outputs <derivation>: fetch the store paths associated with a derivation - 
nix-store --query --deriver <store-path>: fetch the derivation that a store path originates from - 
nix-store --realize <derivation>: build (realise) a derivation; create store paths from outputs - 
nix derivation show <derivation>: inspect a derivation in human-readable JSON form - 
/run/current-system: the NixOS system profile; same as/nix/var/nix/profiles/system - 
~/.nix-profile: the NixOS user profile 
Here are some bonus commands that were not needed but very useful in day-to-day store operation:
- 
nix-store --query --referrers <store-path>: see what references a store path - 
nix-store --query --roots: roots of a store path - 
nix-store --query --graph: produce a neat Graphviz DOT representation of a dependency graph for a package 
For further reading, I suggest looking into the official NixOS documentation.
Bonus: Garbage Collection
Ever wondered how the Nix store is able to know which store paths need to be deleted? In case you weren’t aware, garbage collection allows us to delete old store paths by running nix-collect-garbage -d.
So far, we know that the relation is essentially profile ↔ store ↔ deriver. As it was explained before, store paths are immutable by design. Garbage collection simply identifies every store path that is not reachable from a set of GC roots, and deletes them.
Since every profile generation in ~/.nix-profile is considered a root in and of itself, as long as it exists, these store paths that we saw are considered new. If you build a new generation, the old generations still exist until you delete them or they get automatically deleted, so their store paths are still referenced. In other words, if you want old packages actually removed, you must remove or prune those old generations and then run the garbage collector.