You heard about Nix. You installed it. Now you keep hearing "nix-shell" everywhere. What does it actually do?

Most people install Nix and immediately try to use it like apt — permanently installing packages. That works, but it misses the point. The real power of Nix is temporary, isolated environments that disappear when you're done. That's what nix-shell does.

This post covers everything: what it is, how to set it up, the different ways to use it, and how it fits into the modern Nix workflow with flakes and direnv.


What nix-shell Actually Does

nix-shell drops you into a bash shell where specific packages are available on your PATH — but only for that session. Leave the shell and everything vanishes. No permanent install. No pollution. No conflicts.

Think of it as a temporary workspace. You tell Nix what tools you need, Nix builds or downloads them into the Nix store, sets up the environment variables, and hands you a shell. The packages live in /nix/store under hashed paths, so they never overwrite anything. Two projects needing different versions of Python? No problem — each nix-shell gets its own isolated copy.

The core idea: reproducible environments without permanent changes.


Requirements: Making nix-shell Available

Before using nix-shell, you need to make sure it's actually on your PATH. The setup differs depending on how you installed Nix.

On NixOS

nix-shell works out of the box. NixOS ships with Nix as part of the system, so the command is already available in every shell session. No configuration needed for classic nix-shell.

If you want to use flakes (nix develop), you need to enable experimental features in configuration.nix:

# /etc/nixos/configuration.nix
{
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
}

Then rebuild:

sudo nixos-rebuild switch

On Non-NixOS (Standalone Nix Install)

The Nix installer automatically adds Nix to your shell profile. When you install Nix, it creates a script at ~/.nix-profile/etc/profile.d/nix.sh that sets up your PATH and other environment variables.

On most systems, your .bashrc or .zshrc already sources this script (the installer adds the line). But if it doesn't, add it manually:

# Add to ~/.bashrc or ~/.zshrc
if [ -e ~/.nix-profile/etc/profile.d/nix.sh ]; then
    source ~/.nix-profile/etc/profile.d/nix.sh
fi

After adding this, restart your shell or run:

source ~/.bashrc

Verify it works:

which nix-shell
# Should output: /home/youruser/.nix-profile/bin/nix-shell

Checking Your PATH

If nix-shell isn't found, check that ~/.nix-profile/bin is in your PATH:

echo $PATH | tr ':' '\n' | grep nix

If nothing shows up, Nix isn't properly set up. Either the installer didn't add the profile script, or your shell isn't sourcing it. On NixOS this shouldn't happen — if it does, check your environment.systemPackages or shell configuration.

The NIX_PATH Variable

Some shell.nix files use <nixpkgs> syntax, which relies on the NIX_PATH environment variable to locate the Nixpkgs repository. Without it, you'll see:

error: file 'nixpkgs' was not found in the Nix search path (add it using $NIX_PATH or -I)

On NixOS, NIX_PATH is set automatically through the NixOS module system. On standalone Nix installs, the installer sets it up. If it's missing, you can configure it:

NixOS — in configuration.nix:

{
  nix.nixPath = [ "nixpkgs=/run/current-system/sw/share/nixpkgs" ];
}

Standalone Nix — in .bashrc:

export NIX_PATH="$HOME/.nix-defexpr/channels:$NIX_PATH"

Summary: What You Need Per Context

Context nix-shell on PATH NIX_PATH Flakes (nix develop)
NixOS ✅ Automatic ✅ Automatic Enable experimental-features
Standalone Nix (multi-user) ✅ Installer sets up ✅ Installer sets up Enable experimental-features
Standalone Nix (single-user) ✅ Installer sets up May need manual setup Enable experimental-features
Docker/NixOS container ⚠️ Depends on image ⚠️ Depends on image Usually disabled

Ad-hoc Mode: One-Liners

The fastest way to use nix-shell is with the -p flag. Tell it what packages you need:

nix-shell -p git curl python3

This gives you a shell where git, curl, and python3 are available. Type exit or press Ctrl+D to leave — the packages are gone.

Run a single command without entering a shell:

nix-shell -p cowsay --run "cowsay hello"

Nest shells for extra tools:

nix-shell -p nodejs
# Inside that shell:
nix-shell -p ripgrep fzf
# Now you have nodejs + ripgrep + fzf
# Exit one level to lose ripgrep and fzf, keep nodejs

This is great for quick experiments. No files to create, no config to manage. Just type the command.


Declarative Mode: shell.nix

Ad-hoc is fine for one-offs. For a project you'll work on repeatedly, write a shell.nix file at the project root:

# shell.nix
{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShellNoCC {
  packages = with pkgs; [
    python3
    black
    mypy
    git
  ];
}

Now anyone who clones this project and runs nix-shell gets the exact same environment. The file goes in version control alongside the code.

mkShell vs mkShellNoCC

  • mkShellNoCC — no C compiler toolchain. Use for interpreted languages (Python, JavaScript, Go).
  • mkShell — includes gcc, make, binutils. Use for C/C++ projects or anything that needs compilation.
# C/C++ project
pkgs.mkShell {
  packages = with pkgs; [
    cmake
    pkg-config
    openssl
  ];
  buildInputs = [ ];
  nativeBuildInputs = [ ];
}

Environment Variables

Any non-reserved attribute passed to mkShellNoCC becomes an environment variable:

pkgs.mkShellNoCC {
  packages = [ pkgs.python3 ];

  MY_VAR = "hello";
  DATABASE_URL = "localhost:5432";
}

Enter the shell and check:

nix-shell
echo $MY_VAR
# hello

shellHook: Startup Commands

Run commands automatically when entering the shell:

pkgs.mkShellNoCC {
  packages = [ pkgs.python3 ];

  shellHook = ''
    echo "Python environment loaded"
    python3 --version
  '';
}

Use shellHook for initialization that shouldn't be part of the derivation — printing messages, activating virtualenvs, setting derived variables.


Pure Mode: Isolating from Your Host

By default, nix-shell inherits your current environment. Your PATH, your editor, your git config — everything comes along. Sometimes that's fine. Sometimes it breaks reproducibility.

Use --pure to strip the environment:

nix-shell --pure -p python3

Now only Nix-provided packages are available. Your system-installed git, node, vim — all invisible. This matches what nix-build sees, making it ideal for debugging build failures.

Keep specific variables in pure mode:

nix-shell --pure --keep HOME --keep EDITOR -p gcc

HOME, USER, and DISPLAY are kept by default. Use --keep for anything else you need.


Drill Session: Build Comfort Before You Move On

If you're new to nix-shell, reading about it isn't the same as feeling it. Before moving into flakes and direnv, run through this short sequence. Each step builds on the last, and the whole thing takes about ten minutes.

1. Confirm a package is gone before you start.

which htop
# Should print nothing, or "not found"

2. Enter an ad-hoc shell and confirm the package appears.

nix-shell -p htop
which htop
# Now prints a /nix/store path

3. Exit and confirm it's gone again.

exit
which htop
# Back to nothing

This round trip is the entire mental model: nothing is installed, nothing is left behind.

4. Write your first shell.nix.

Create a folder for this practice, and inside it, shell.nix:

# shell.nix
{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShellNoCC {
  packages = with pkgs; [ htop cowsay ];
  shellHook = ''
    echo "Drill shell loaded — htop and cowsay are ready."
  '';
}

Run nix-shell with no arguments inside that folder. Confirm the greeting prints and both htop and cowsay work.

5. Break it on purpose.

Run nix-shell --pure in the same folder. Try cowsay hello — it should still work, since it came from the Nix store. Now try something you know is only installed on your host system, like your system editor (code, vim, nano) or git if you didn't add it to packages. Watch it fail. This is the difference between "works on my machine" and "works because Nix says so" — and it's the same gap nix-build and CI systems live in.

6. Clean up.

Exit the shell and delete the practice folder. Nothing was installed on your system, so there's nothing to uninstall — that's the point.

If all six steps made sense, you've internalized the core idea. Everything from here — nix develop, direnv, shebang scripts — is a more convenient way of doing exactly this.


nix-shell vs nix develop

If you're using flakes (and you probably should be), nix develop replaces nix-shell for development environments.

  nix-shell nix develop
Config file shell.nix flake.nixdevShells
Nixpkgs resolution <nixpkgs> via NIX_PATH or pinned fetchTarball Pinned in flake.lock
Speed 1-3 seconds (evaluates Nixpkgs each time) Faster (cached evaluation)
Reproducibility Depends on NIX_PATH Fully pinned via flake.lock
Status Stable, legacy New CLI, experimental features required

Basic flake equivalent of shell.nix:

# flake.nix
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      devShells.${system}.default = pkgs.mkShellNoCC {
        packages = [ pkgs.python3 pkgs.black ];
      };
    };
}

Run with:

nix develop

Reusing shell.nix from a Flake

You can wrap an existing shell.nix in a flake without rewriting everything:

# flake.nix
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      devShells.${system}.default = import ./shell.nix { inherit pkgs; };
    };
}

This gives you flake speed with your existing shell.nix config.

nix develop for Debugging Packages

nix develop also works for inspecting any derivation's build environment:

nix develop nixpkgs#hello

Inside, you get the exact environment Nix uses to build hello — all dependencies, environment variables, and shell functions. Run build phases manually:

unpackPhase
configurePhase
buildPhase

This is invaluable for debugging why a package fails to build.


Shebang Scripts: Portable Scripts with Dependencies

nix-shell can act as a script interpreter. Write a script that brings its own dependencies:

#!/usr/bin/env nix-shell
#! nix-shell -i python3 -p python3 python3Packages.requests

import requests

r = requests.get("https://httpbin.org/get")
print(r.status_code)

Make it executable and run it:

chmod +x script.py
./script.py

Nix downloads the dependencies, invokes Python, and runs your script. On any machine with Nix installed. No pip install needed.

Pin nixpkgs for exact reproducibility:

#!/usr/bin/env nix-shell
#! nix-shell -i bash -p git
#! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/aed4b19d312525ae7ca9bceb4e1efe3357d0e2eb.tar.gz

git log --oneline -5

Flake-based shebang (Nix 2.19+):

#!/usr/bin/env -S nix shell nixpkgs#hello nixpkgs#cowsay --command bash

hello | cowsay

direnv Integration: Auto-Activate on cd

Manually running nix-shell every time you enter a project is tedious. direnv automates it — cd into a directory and the Nix environment activates automatically. Leave and it deactivates.

Basic Setup

1. Install direnv:

nix-env -iA nixpkgs.direnv
# Or on NixOS:
# environment.systemPackages = [ pkgs.direnv ];

2. Add direnv hook to your shell:

For bash, add to ~/.bashrc:

eval "$(direnv hook bash)"

For zsh, add to ~/.zshrc:

eval "$(direnv hook zsh)"

3. Create .envrc in your project:

echo "use nix" > .envrc
direnv allow

4. That's it. Next time you cd into the directory, the shell loads automatically. Leave and it unloads.

With nix-direnv (Faster)

The default use nix can be slow on first load. nix-direnv caches the environment and prevents garbage collection:

On NixOS:

# configuration.nix
{
  programs.direnv.enable = true;
  programs.direnv.nix-direnv.enable = true;
}

Note: programs.direnv.enable turns on nix-direnv by default on current NixOS releases, so the second line is technically redundant. It's still worth writing explicitly — it documents intent and protects you if that default ever changes.

Standalone Nix — in .envrc:

if ! has nix_direnv_version || ! nix_direnv_version 3.1.2; then
  source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.1.2/direnvrc" "sha256-Di03ad3a0ueGi6CGrfhrQzyGdQIg9APXIPCAMNQgWYM="
fi

use nix

Flake-based direnv

For flake projects, use use flake instead of use nix:

echo "use flake" > .envrc
direnv allow

direnv calls nix develop under the hood, loading the devShells.default output from your flake.nix.


Debugging Derivations with nix-shell

One of nix-shell's original purposes: debugging package builds. When a build fails, drop into its environment and run steps manually.

nix-shell '<nixpkgs>' --attr hello

Inside the shell, source the setup and run phases:

source $setup
unpackPhase
cd $sourceRoot
configurePhase
buildPhase

If something fails at configurePhase, you can inspect the environment, check variables, and try things manually. This is much faster than adding print statements to a Nix expression and rebuilding.

Working with stdenv

Nixpkgs uses stdenv — a standard environment that provides common build tools and phases. Inside nix-shell, these phases are available as shell functions:

  • unpackPhase — extracts source
  • patchPhase — applies patches
  • configurePhase — runs configure scripts
  • buildPhase — compiles
  • checkPhase — runs tests
  • installPhase — installs to $out
  • fixupPhase — patches binaries, strips debug info

Run them individually or chain them:

unpackPhase && cd $sourceRoot && configurePhase && buildPhase

Common Pitfalls

"nix-shell: command not found"

Nix isn't on your PATH. Fix per the Requirements section above. On NixOS, this shouldn't happen.

"file 'nixpkgs' was not found in the Nix search path"

NIX_PATH isn't set. Your shell.nix uses <nixpkgs> which needs this variable. Either set NIX_PATH or switch to fetchTarball with a pinned URL.

Purity Errors When Linking

ld: warning: ignoring file .../libfoo.so: file was built for newer macOS version

or

refusing to link against file not in nix store

Nix's linker wrapper rejects files outside /nix/store. For local development, disable purity checking:

NIX_ENFORCE_PURITY=0 nix-shell

GTK/GSettings Crashes in nix-shell

No GSettings schemas are installed on the system

Add to your mkShell:

mkShell {
  buildInputs = [ pkgs.gtk3 ];
  shellHook = ''
    export XDG_DATA_DIRS=$GSETTINGS_SCHEMAS_PATH
  '';
}

Icons Not Rendering

Similar fix:

shellHook = ''
  export XDG_DATA_DIRS=...:${pkgs.hicolor-icon-theme}/share:${pkgs.gnome3.adwaita-icon-theme}/share
'';

Sharing Dependencies Between Build and Dev Shell

If you have a default.nix that builds a package, you can share its dependencies with your dev shell using inputsFrom:

# default.nix
let
  pkgs = import <nixpkgs> {};
  myPackage = pkgs.callPackage ./package.nix {};
in {
  inherit myPackage;
  shell = pkgs.mkShellNoCC {
    inputsFrom = [ myPackage ];
    packages = [ pkgs.python3 ];  # extra dev tools
  };
}
# shell.nix
(import ./.).shell

Now nix-shell gives you everything myPackage needs to build, plus your extra dev tools. Change dependencies in one place, both build and dev shell stay in sync.


Quick Reference

Command What it does
nix-shell -p git Shell with git available
nix-shell -p python3 --run "python3 script.py" Run command, exit
nix-shell --pure -p gcc Isolated shell, no host pollution
nix-shell Read shell.nix in current dir
nix develop Read flake.nix devShells
nix develop nixpkgs#hello Debug hello's build environment
nix-shell -i python3 -p python3 requests Shebang script support

Where to Go From Here

nix-shell is the gateway to Nix's real value: reproducible, isolated environments. Once you internalize "I can get any tool without installing it permanently," the workflow becomes addictive.

Start with nix-shell -p for quick experiments. Move to shell.nix for project-specific environments. Add direnv to stop typing nix-shell altogether. And when you're ready for full reproducibility, wrap it in a flake.nix and pin everything.

The Nix ecosystem is big. nix-shell is where most people actually start using it.