Nix Shells and the devShell Concept
Nix Shells and the devShell Concept
Compilers on the host (apt install golang, brew install go) rot the next repo. The boring default is: pkgs.mkShell in the repo, entered with nix develop (flake) or nix-shell (classic), gone when you exit.
Mental model
A devShell is a child environment, not an install:
- Nix realises the listed packages into
/nix/store. - It prepends their
bin/toPATH(and setsPKG_CONFIG_PATH, etc.). - It runs
shellHook. exitdrops every injection. The host/usris unchanged.
host PATH = /usr/bin
│
├── nix develop / nix-shell
│ PATH = /nix/store/…-go/bin:/nix/store/…-jq/bin:…
│ shellHook
└── exit → host PATH again
| File | Command |
|---|---|
shell.nix |
nix-shell (often still <nixpkgs>) |
flake.nix devShells |
nix develop + flake.lock — desk default |
packages on mkShell is the modern list of tools. buildInputs / nativeBuildInputs still work and matter when you compile C in the shell.
Worked examples
Case 1: Classic shell.nix
Save as shell.nix:
# shell.nix
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
packages = [
pkgs.ripgrep
pkgs.jq
];
shellHook = ''
echo "=== Desk Shell Ready ==="
'';
}nix-shell --run 'which jq && jq --version'Output (shape):
=== Desk Shell Ready ===
/nix/store/…-jq-…/bin/jq
jq-1.7.1
Outside the shell, which jq may be missing or a different binary.
Case 2: Declared environment
Save as env_shell.nix:
# env_shell.nix
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
packages = [ pkgs.curl ];
env = {
DESK_API_URL = "https://api.desk.internal";
DESK_TIMEOUT = "30";
};
shellHook = ''
echo "Targeting API: $DESK_API_URL"
'';
}nix-shell env_shell.nix --run 'echo Timeout: $DESK_TIMEOUT'Output:
Targeting API: https://api.desk.internal
Timeout: 30
Prefer env. over export in shellHook so the variables are structured. Do not put secrets here.
Case 3: One-shot without a file
nix shell github:NixOS/nixpkgs/nixos-26.05#jq --command jq --versionUseful for five minutes. A repo still needs a committed shell so CI and humans match.
Case 4: Flake devShells.default
# flake.nix
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in
{
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [ pkgs.go pkgs.jq ];
};
};
}nix develop --command go versionSame packages every laptop with the same flake.lock. Next chapter.
Case 5: packages vs nativeBuildInputs
pkgs.mkShell {
packages = [ pkgs.go ]; # on PATH
nativeBuildInputs = [ pkgs.pkg-config ]; # also on PATH; marked build-time
buildInputs = [ pkgs.openssl ]; # headers + libs for compiles
}For a Go API with CGO off, packages = [ pkgs.go pkgs.gopls ] is enough. When you compile openssl-using C, you need buildInputs.
The trap
The trap is apt install golang “so I don’t need Nix for a minute.” The minute becomes the project. Two versions of go on PATH; CI uses Nix; you do not.
The other trap is secrets in shellHook (export AWS_SECRET=…). The file is git. Use sops / direnv dotenv that is gitignored.
The boring rule
- One
mkShellper repo. Flake + lock when you can. packagesfor CLIs.buildInputsfor libraries you link.env.for non-secret variables.shellHookforechoandalias.exitmust restore the host. If it does not, you exported into the parent (you usednix-shellwrong, or direnv — part 3 later).- No compilers via apt/brew for desk work.
Try this
- Add
pkgs.helloto Case 1;nix-shell --run hello. which jqinside vs afterexit.nix-shell --pure --run 'echo $PATH'and notice/usr/binis gone or last —--pureis how you catch host leaks.- Put
DESK_TOKENinenvthen delete it; use a gitignored.envinstead if you needed it.