Declarative shell environments with shell.nix#


Declarative shell environments allow you to:

  • Automatically run bash commands during environment activation

  • Automatically set environment variables

  • Put the environment definition under version control and reproduce it on other machines

What will you learn?#

In the Ad hoc shell environments tutorial, we looked at imperatively creating shell environments using nix-shell -p, when we need a quick way to access tools without having to install them globally. We also saw how to execute that command with a specific Nixpkgs revision using a Git commit as an argument, to recreate the same environment used previously.

In this tutorial we’ll take a look at how to create reproducible shell environments given a declarative configuration in a Nix file.

How long will it take?#

30 minutes

What will you need?#

Entering a temporary shell#

Suppose we want a development environment in which Git, Neovim, and Node.js were installed. The simplest possible way to accomplish this is via the nix-shell -p command:

$ nix-shell -p git neovim nodejs

This command works, but there’s a number of drawbacks:

  • You have to type out -p git neovim nodejs every time you enter the shell.

  • It doesn’t (ergonomically) allow you any further customization of your shell environment.

A better solution is to create our shell environment from a shell.nix file.

A basic shell.nix file#

Create a file called shell.nix with these contents:

 2  nixpkgs = fetchTarball "";
 3  pkgs = import nixpkgs { config = {}; overlays = []; };
 6pkgs.mkShell {
 7  packages = with pkgs; [
 8    git
 9    neovim
10    nodejs
11  ];
Detailed explanation

We use a version of Nixpkgs pinned to a release branch, and explicitly set configuration options and overlays to avoid them being inadvertently overridden by global configuration.

mkShell is a function that produces a shell environment. It takes as argument an attribute set. Here we give it an attribute packages with a list containing one item from the pkgs attribute set.

Side note on packages and buildInputs

You may encounter examples of mkShell that add packages to the buildInputs or nativeBuildInputs attributes instead.

nix-shell was originally conceived as a way to construct a shell environment containing the tools needed to debug package builds. Only later it became widely used as a general way to make temporary environments for other purposes.

mkShell is a wrapper around mkDerivation, so it takes the same arguments as mkDerivation, such as buildInputs or nativeBuildInputs. The packages attribute argument to mkShell is simply an alias for nativeBuildInputs.

Enter the environment by running nix-shell in the same directory as shell.nix:

$ nix-shell

nix-shell by default looks for a file called shell.nix in the current directory and builds a shell environment from the Nix expression in this file. Packages defined in the packages attribute will be available in $PATH.

Check that the desired packages are indeed available in the expected version as we did in the previous tutorial.

Environment variables#

You may want to automatically export certain environment variables when you enter a shell environment.

Set your GIT_EDITOR to use the nvim from the shell environment:

   nixpkgs = fetchTarball "";
   pkgs = import nixpkgs { config = {}; overlays = []; };

 pkgs.mkShell {
   packages = with pkgs; [

+  GIT_EDITOR = "${pkgs.neovim}/bin/nvim";

Any attribute name passed to mkShell that is not reserved otherwise and has a value which can be coerced to a string will end up as an environment variable.

Detailed explanation

The newly added attribute GIT_EDITOR is set to a string composed of the output store path of the neovim derivation and the relative path to the nvim executable inside that store path.

See the Nix language tutorial on derivations for details.


Some variables are protected from being set as described above.

For example, the shell prompt format for most shells is set by the PS1 environment variable, but nix-shell already sets this by default, and will ignore a PS1 attribute set in the argument.

If you really need to override these protected environment variables, use the shellHook attribute as described in the next section.

Startup commands#

You may want to run some commands before entering the shell environment. These commands can be placed in the shellHook attribute provided to mkShell.

Set shellHook to output the current repository status:

   nixpkgs = fetchTarball "";
   pkgs = import nixpkgs { config = {}; overlays = []; };

 pkgs.mkShell {
   packages = with pkgs; [

   GIT_EDITOR = "${pkgs.neovim}/bin/nvim";
+  shellHook = ''
+    git status
+  '';