I run NixOS on three machines and I have the same set of tools on all of them. Not because I followed a guide, but because I hit the same friction points on each one and eventually fixed them the same way. This is that list.
comma — Run Software Without Installing It
comma is the one I show people first. You put a , in front of any command, and it finds the right package in nixpkgs, pulls it in temporarily, and runs it. Nothing gets installed permanently. Nothing pollutes your profile.
, cowsay "it just works", cowsay "it just works"
Under the hood it wraps nix shell -c and nix-index together. It also caches results: once you pick a derivation for a command, it remembers, and once the path is evaluated by Nix, subsequent calls are nearly instant. The benchmarks in the repo show cache level 2 is about 159x faster than cache level 1. That number is real.
It requires a nix-index database to know where files live in nixpkgs. The companion project nix-index-database provides pre-generated databases on a regular schedule, so you can skip building one yourself.
nix-index — Find What Package Owns a File
nix-index builds a local database by indexing the binary caches. Then you query it with nix-locate to find which nixpkgs package contains a specific file.
$ nix-locate 'bin/hello' hello.out 29,488 x /nix/store/bdjyhh70...-hello-2.10/bin/hello$ nix-locate 'bin/hello' hello.out 29,488 x /nix/store/bdjyhh70...-hello-2.10/bin/hello
The part most people actually use is the command-not-found integration. When you type a command that is not installed, your shell tells you the exact package to install instead of just printing an error. Coming from a traditional distro, this single feature makes NixOS feel less hostile.
Building the index takes around five minutes. Or skip it and use nix-index-database for a pre-built one.
nurl — Generate Nix Fetcher Calls from URLs
Writing a fetcher call by hand means figuring out the right fetcher, the right attributes, and then computing a hash by actually downloading the source. It is annoying every single time.
nurl takes a URL and an optional revision and prints a ready-to-use fetcher call with the hash filled in.
$ nurl https://github.com/nix-community/patsh v0.2.0 fetchFromGitHub { owner = "nix-community"; repo = "patsh"; tag = "v0.2.0"; hash = "sha256-7HXJspebluQeejKYmVA7sy/F3dtU1gc4eAbKiPexMMA="; }$ nurl https://github.com/nix-community/patsh v0.2.0 fetchFromGitHub { owner = "nix-community"; repo = "patsh"; tag = "v0.2.0"; hash = "sha256-7HXJspebluQeejKYmVA7sy/F3dtU1gc4eAbKiPexMMA="; }
It supports fetchFromGitHub, fetchFromGitLab, fetchFromGitea, fetchFromSourcehut, fetchCrate, fetchPypi, fetchhg, fetchsvn, and more. It avoids slow fixed-output derivations when faster alternatives exist. And it is the foundation that nix-init builds on.
nix-init — Generate Full Nix Packages from URLs
If nurl handles the fetching, nix-init handles everything else. Point it at a URL and it generates a complete package expression with hash prefetching, dependency inference, and license detection. All through interactive prompts with fuzzy tab completion.
Supported builders: stdenv.mkDerivation, buildRustPackage, buildPythonApplication, buildPythonPackage, and buildGoModule. For Rust projects it infers the cargo hash automatically. For Python it picks up dependencies from PyPI metadata.
The docs are honest about this: the output will probably need tweaks, and you should verify the license and description yourself. But it does the 80% that nobody wants to do manually, which is why packaging contributions happen at all.
nh — A Better CLI for NixOS, Home Manager, and Darwin
nh reimplements nixos-rebuild, home-manager switch, and darwin-rebuild with better output and a few genuinely useful additions.
| Platform | Without nh | With nh |
|---|---|---|
| NixOS | nixos-rebuild switch --flake .#myHost | nh os switch . -H myHost |
| Darwin | darwin-rebuild switch --flake .#myHost | nh darwin switch . -H myHost |
| Home Manager | home-manager switch --flake .#myHost | nh home switch . -c myHome |
The shorter syntax is nice but not the point. When you run nh os switch, you get a build tree from nix-output-monitor, a diff of what is actually changing in your derivations, and a confirmation prompt before anything gets activated. You stop switching blind.
nh clean extends nix-collect-garbage with gcroot cleanup, profile targeting, and time-based retention like --keep-since 4d. A NixOS module in nixpkgs can wire this up as an automatic service.
statix — Lint Your Nix Code
statix checks Nix files for antipatterns and fixes the ones it can.
$ statix check tests/c.nix [W04] Warning: Assignment instead of inherit from ╭─[tests/c.nix:2:3] │ 2 │ mtl = pkgs.haskellPackages.mtl; · ─────────── This assignment is better written with inherit$ statix check tests/c.nix [W04] Warning: Assignment instead of inherit from ╭─[tests/c.nix:2:3] │ 2 │ mtl = pkgs.haskellPackages.mtl; · ─────────── This assignment is better written with inherit
$ statix fix --dry-run tests/c.nix - mtl = pkgs.haskellPackages.mtl; + inherit (pkgs.haskellPackages) mtl;$ statix fix --dry-run tests/c.nix - mtl = pkgs.haskellPackages.mtl; + inherit (pkgs.haskellPackages) mtl;
It catches redundant pattern bindings, collapsible let...in blocks, manual inherit patterns, unnecessary boolean comparisons, unquoted URIs, and more. The full list is available via statix list. Specific lints can be disabled in a statix.toml at the project root, and it respects .gitignore by default.
Plug it into your editor and forget about it.
nix-direnv — Fast, Persistent Dev Shells
nix-direnv replaces direnv's built-in use_nix and use_flake. The built-in version re-evaluates on every shell entry, which gets slow. nix-direnv caches the result and only rebuilds when your shell.nix or flake.nix actually changes.
The part I care about more: it pins the shell derivation in your Nix gcroots. Garbage collection will not delete your build dependencies. I lost hours of cached downloads to a nix-collect-garbage run before I started using this.
Setup with Home Manager:
programs.direnv = { enable = true; nix-direnv.enable = true; };programs.direnv = { enable = true; nix-direnv.enable = true; };
Add use flake or use nix to .envrc, run direnv allow, and your environment loads automatically on cd. Works with classic shell.nix and flake-based devShells.
flake-parts — The Module System for Your flake.nix
Every non-trivial flake.nix eventually becomes a mess. Packages for multiple systems, devShells, CI checks, a NixOS module. You end up with 300 lines of forEachSystem repetition and nobody wants to touch the file.
flake-parts brings the NixOS module system to flakes. You split the configuration into focused files and flake-parts assembles them. It handles the system boilerplate and makes it easier for others to pull your flake's outputs into theirs.
Migration is gradual. Wrap your existing outputs in mkFlake and move things over piece by piece:
outputs = inputs@{ flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } { systems = [ "x86_64-linux" "aarch64-linux" ]; # perSystem and other modules go here };outputs = inputs@{ flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } { systems = [ "x86_64-linux" "aarch64-linux" ]; # perSystem and other modules go here };
Projects using it include nixd, argo-workflows, and hyperswitch. It has become a standard approach for flakes that need to stay maintainable.
hjem — Home File Management Without the Overhead
If you have ever wanted Home Manager's home.file without the rest of Home Manager, hjem is worth knowing about.
Hjem ("home" in Danish) is a NixOS module that manages files in $HOME. That is mostly it. No program modules, no environment activation scripts, no opinion about your shell. You declare files and it links them into place using smfh, an atomic file linker backed by systemd services.
hjem = { users.k1ng = { enable = true; files = { ".foo".text = "bar"; ".config/something.json" = { generator = lib.generators.toJSON { }; value = { foo = 1; bar = "hello"; }; }; }; }; };hjem = { users.k1ng = { enable = true; files = { ".foo".text = "bar"; ".config/something.json" = { generator = lib.generators.toJSON { }; value = { foo = 1; bar = "hello"; }; }; }; }; };
It is multi-user by design. Each key under hjem.users maps to a user in users.users, and hjem refuses to manage homes for users that do not exist.
The honest comparison to Home Manager: hjem does far less. It does not abstract away program configuration. There is no programs.git.enable equivalent. The project is explicit that application-specific modules are out of scope, though a separate project called hjem-rum is building that layer on top.
I think that constraint is actually the point. Home Manager is large, occasionally opinionated in ways you did not ask for, and has a reputation for activation scripts that sometimes conflict with NixOS's own. Hjem is small enough that you can read the entire codebase in an afternoon. If all you want is declarative dotfile management wired into your NixOS config without pulling in a second module framework, hjem is the cleaner answer. It is still young at around 330 stars, so I would not call it battle-tested, but it is actively maintained and the scope is intentionally narrow enough that "mostly feature-complete" is a realistic claim.
NixOS Options Search — Keep This Tab Open
Not something you install: search.nixos.org/options.
It searches the entire set of NixOS module options. Type, default value, which module declares it. When you cannot remember if it is services.nginx.enable or programs.nginx.enable, this is where you go. It is faster than reading source, and honestly faster than grepping nixpkgs.
It also covers Home Manager options and has a package search tab. I have it open on every machine I work on.
None of these are required. NixOS runs fine without them. But each one fixes something that is genuinely annoying without it, and after a while you stop noticing the friction because you stopped having it.
Comments