Jack K

Batou (left) and Major Kusanagi Motoko (right) from "Ghost in the Shell" in front of a large building, with show title at the bottom modified to swap "Ghost" for "Nix". The characters also feature the Nix logo (snowflake made of 6 lamdas alternating between dark and light blue) over their eyes.

Nix in the Shell

So, maybe you read the previous installment on what Nix is; or you just want to get stuck in with Nix. As long as you've installed Nix or can run it in a container we can get started.

Table of Contents

A Basic Shell

For this example I'll be using flakes and the new cli. (If you'd like to learn more about flakes vs before flakes, there will be a future blog post.) For now you can try follow the steps below, and if you get a warning you need "Experimental features" and aren't interested in enabling them in your configuration, just add --extra-experimental-features "nix-command flakes" after nix.

Our basic minimal shell.nix file can appear like so:

{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  packages = [
    pkgs.protobuf
    pkgs.hadolint
    pkgs.nodejs
  ];
}

At this point if you had this file and ran nix-shell if you didn't have any of these tools, they'll now be available to you. And in the case of openssl you'd have the libraries and headers. And if you did have them available you should find your current PATH will pull the ones from the nix store. You can see this by running readlink -f $(which BINARY):

$ readlink -f $(which protoc)
/nix/store/82dkd41796gz81i8nqx70xjcajy32p3q-protobuf-31.1/bin/protoc-31.1.0

$ sha256sum $(which protoc)
92d2bf5ca23e8cdf3a39feb5433970e11b871874e3c44a0c636face2a3801843  /nix/store/82dkd41796gz81i8nqx70xjcajy32p3q-protobuf-31.1/bin/protoc

shell.nix Annotated

For this guide we'll just make some simple tweaks, but if you're interested in what's going on here's an annotated version of the same file:

# Overall this is a function.
#
# The output produced by "evaluating" and building the
# output (AKA derivation) will match what `nix-shell` expects.
#
# It takes a `pkgs` argument with a default provided of
# `import <nixpkgs> {}`.
{
  pkgs ?
    # `import` is a way of calling other nix files.
    import
      # `<nixpkgs>` is a special syntax for refering to "channels"
      # you can define, think of them like aliases.
      #
      # And because the `default.nix` within the `nixpkgs` repository
      # is itself a function we provide an empty `{}` object.
      # In nix objects are called an "attribute set".
      <nixpkgs> {}
}:
# From `pkgs` we run the function `mkShell`.
pkgs.mkShell
  # `mkShell` expects an attribute set where most
  # attributes/parameters are optional.
  {
    # Here we use the `packages` attribute to define what
    # to provide in the shell.
    packages = [
      # and from `pkgs` we have access to
      # `protobuf`, `hadolint`, and `nodejs`
      pkgs.protobuf
      pkgs.hadolint
      pkgs.nodejs
    ];
  }

Pinning our copy of nixpkgs

With the simple shell.nix if you run nix-shell on an x86_64 Linux machine, and your friend runs the same nix-shell command with the same shell.nix you can still end up with different versions or copies of binaries. This can be demonstrated by the fact you probably have a different copy to me in my example above.

To ensure you get not only the same version of protoc e.g. 31.1.0 , but also the exact same file e.g. it lives in /nix/store/82dkd41796gz81i8nqx70xjcajy32p3q-protobuf-31.1/ and the same sha256sum we'll want to do some "pinning" or "locking. This is basically standard practice now across ecosystems for package management. You may recognise files like package-lock.json, go.sum, Cargo.lock etc. Lock files (to varying degrees of success) are intended to identify the exact same dependency every single time.

We can do the same with our small shell.nix:

{ pkgs ?
  import (builtins.fetchTarball {
    # Descriptive name to make the store path easier to identify
    name = "nixos-unstable-2025-08-19";
    # Commit hash for nixos-unstable as of 2025-08-19
    url = "https://github.com/nixos/nixpkgs/archive/20075955deac2583bb12f07151c2df830ef346b4.tar.gz";
    # Hash obtained using `nix-prefetch-url --unpack <url>`
    sha256 = "1s3lxb33cwazlx72pygcbcc76bbgbhdil6q9bhqbzbjxj001zk0w";
  }) {}
}:
pkgs.mkShell {
  packages = [
    pkgs.protobuf
    pkgs.hadolint
    pkgs.nodejs
  ];
}

Now we will always fetch the 20075955deac2583bb12f07151c2df830ef346b4 commit of NixOS/nixpkgs (which was the current latest commit as of writing this post.) By forcing this exact commit we're ensuring that everything is the same for everyone who runs this.

This inline and more manual method of handling our dependency on nixpkgs can be difficult, especially if we want other remote nix code for overlays etc. This is one aspect where flakes can help, or you can look at projects like npins which work with pre-flakes tooling like nix-shell.

You may also be wondering, should I use nixpkgs-unstable, nixos-unstable, nixos-25.05, nixos-25.05-small, etc. I plan to cover this in more detail in the future but for getting started any release should work. Just make sure when you're doing a search on https://search.nixos.org you pick a matching release!

Adding Proprietary tooling

Some times you might need some proprietary tooling, which will also be called "unfree". For this example we'll use unrar. The UnRAR license explicitly says it's "freeware" so better matches "source-avalable" licenses. It wouldn't be considered Free or Open Source because of it's section 2 relating to the proprietary RAR compression algorithm, and it's section 6 relating to deleting all "UnRAR files from your storage devices and cease to use the utility."

(If you're interested in licenses and SPDX, it's actually yet to be reviewed.)

By default nixpkgs won't give access to "unfree" software as a saftey mechanism and to make "unfree" software opt-in. There are several ways to do this but if we'd like our choices to be preserved in shell.nix we can do this:

{ pkgs ?
  import (builtins.fetchTarball {
    # Descriptive name to make the store path easier to identify
    name = "nixos-unstable-2025-08-19";
    # Commit hash for nixos-unstable as of 2025-08-19
    url = "https://github.com/nixos/nixpkgs/archive/20075955deac2583bb12f07151c2df830ef346b4.tar.gz";
    # Hash obtained using `nix-prefetch-url --unpack <url>`
    sha256 = "1s3lxb33cwazlx72pygcbcc76bbgbhdil6q9bhqbzbjxj001zk0w";
  }) {
    config.allowUnfree = true; # this addition
  }
}:
pkgs.mkShell {
  packages = [
    pkgs.protobuf
    pkgs.hadolint
    pkgs.nodejs
    pkgs.unrar
  ];
}

Providing config.allowUnfree = true; when we import nixpkgs allows access to all packages marked "unfree".

The same thing could have also been written like below:

{
  config = {
    allowUnfree = true;
  }
}

If you'd like to be more granular you can instead provide a function to allowUnfreePredicate. In this case our function can receive a pkg and we can either allow it or deny it. In this case we're using the helper lib.getName to pull the package's name, and we're using builtins.elem to do that check against every string in our list. Currently with only "unrar" but we could add other tooling as necessary.

config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [
  "unrar"
];

Graphical tools

Graphical tooling can be a bit more difficult due to how graphics libraries and drivers work. On a NixOS machine there shouldn't be any problems, but on other systems you may get errors from OpenGL or Vulkan.

That's where the nix-community/nixGL project can help. But for graphical tooling outside of NixOS you may find it easier to stick with Flatpak on Linux, or Brew on Darwin/MacOS.

Conclusion

With these snippets we have enough to get a bunch of useful tooling, including the "unfree" stuff where necessary. Feel free to dot these files around and spread the shell.nix file! (if your team is happy with it)

And if cluttering up the root dir is a worry you can put it in a sub directory and run nix-shell ./some/subdir!

In a future post I'll cover doing some of this in a flake.nix, and having the new stuff also be compatible with nix-shell. I'll also touch on packaging tools from scratch!