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— includesgcc,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.nix → devShells |
| 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 sourcepatchPhase— applies patchesconfigurePhase— runs configure scriptsbuildPhase— compilescheckPhase— runs testsinstallPhase— installs to$outfixupPhase— 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.