Project devShell and direnv¶
Overview¶
This walkthrough is a picture-perfect project flake: one devShells.<system>.default built with pkgs.mkShell, plus a .envrc that auto-loads that shell through direnv and nix-direnv. The same tools appear whether you run nix develop manually or cd into the tree with direnv enabled.
Pins such as nixos-26.05 and system = "x86_64-linux" are illustrative—adjust for your machine. Flake workflows need experimental nix-command and flakes. For shell mechanics and CLI comparison, see shells and direnv; for flake output layout, see packages, apps, devShells.
Details¶
Domains composed¶
| Domain | Role in this example |
|---|---|
| Language idioms | let / attrset shape of outputs |
| Garbage collection | Why nix-direnv GC roots matter |
| nix build / develop / run | nix develop vs nix shell |
| direnv / nix-direnv | Auto-enter via .envrc |
| Packages, apps, devShells | devShells.<system>.default |
nix-command / flakes |
Experimental features for flake shells |
| Shells and direnv | Teaching page this walkthrough composes |
| Language toolchains | SDKs inside packages |
File layout¶
A minimal flake-backed dev project looks like this:
my-project/
├── flake.nix # devShells.<system>.default = pkgs.mkShell { … }
├── flake.lock # locked nixpkgs input (commit after first eval)
├── .envrc # use flake (or use nix for classic shell.nix)
└── src/ # application source (optional)
Classic (non-flake) projects swap flake.nix / flake.lock for a single shell.nix and use use nix in .envrc. Shared fixtures: hello-flake/flake.nix (use flake) and shell.nix (use nix) in the example corpus.
Defining devShells.default¶
Expose the shell under the conventional flake path devShells.<system>.default. The value is a derivation from pkgs.mkShell (Nixpkgs manual: pkgs.mkShell):
| Attribute | Use in this walkthrough |
|---|---|
packages |
Preferred for executables on $PATH (git, rustc, linters) |
nativeBuildInputs / buildInputs |
Libraries and build-time deps when mirroring a package’s build env |
inputsFrom |
Pull another derivation’s build inputs into the shell |
shellHook |
Bash run on enter (export, messages)—not evaluated during normal package builds |
Put language SDKs in packages (or scoped wrappers like python3.withPackages) and link to language toolchains for ecosystem-specific shapes—this page does not repeat every language table.
The outputs function in flake.nix is ordinary Nix: let bindings for system and pkgs, then an attrset for devShells (see 03-language idioms for let/rec patterns). No special flake syntax beyond declaring inputs and returning devShells.${system}.default.
Three ways to get tools (do not confuse them)¶
| Entry point | What it does | When to use |
|---|---|---|
nix develop |
Starts bash with the full mkShell / build environment (shellHook, stdenv, inputs) |
One-off hacking, CI scripts, debugging build env |
nix shell |
Puts already-built package binaries on $PATH—no mkShell, no shellHook |
Temporary “I need hello on PATH once” |
| direnv + nix-direnv | Applies the flake devShell (via nix print-dev-env) to your current shell on directory entry |
Day-to-day work—env follows cd |
nix develop requires experimental nix-command; flake refs (., .#ci) also need flakes. Enable both in nix.conf or pass --extra-experimental-features 'nix-command flakes'. Details: nix build / develop / run.
Resolution when you omit #name: nix develop tries devShells.<system>.default, then packages.<system>.default. Named shells: nix develop .#ci → devShells.<system>.ci, then package fallbacks. See packages, apps, devShells.
Classic nix-shell (no flakes) reads shell.nix or default.nix in the project root; see nix-shell.
direnv and nix-direnv¶
direnv watches for .envrc and applies an environment diff when you enter the directory (authorized once with direnv allow). nix-direnv implements use flake and use nix with:
- Caching — after the first successful load, later visits avoid full re-evaluation when inputs are unchanged.
- GC roots — the shell derivation is symlinked into the user’s GC roots so its closure survives
nix-collect-garbagebetween visits.
Under the hood, use flake calls nix print-dev-env on the flake’s default devShell—the same family as nix develop, but merged into your existing shell instead of spawning a subshell. Full behavior: direnv / nix-direnv.
Install nix-direnv globally (NixOS programs.direnv, Home Manager, or a pinned source_url in .envrc) so project .envrc files can stay as short as use flake.
Temporary env vs profile install¶
Dev shells (manual or direnv) are directory/session-scoped. They are not the same as nix profile add, which permanently links packages into a user profile. Use a shell when tools are project-pinned; use a profile only for tools you want everywhere.
Failure modes¶
| Symptom | Likely cause | Fix |
|---|---|---|
experimental feature 'nix-command' is disabled |
Modern CLI off | Enable nix-command (+ flakes for .#) in nix.conf or per command |
error: flake … doesn't provide attribute 'devShells.…' |
Missing or wrong system key |
Match system to nix eval --impure --expr builtins.currentSystem; add that key under devShells |
| direnv: “command not found: use” / stale env | nix-direnv not loaded | Source nix-direnv’s direnvrc globally or pin it from .envrc |
| Tools vanish after GC | Shell env not rooted (stock direnv use_flake) |
Use nix-direnv; it registers GC roots |
“Works in nix develop, fails in CI build” |
Shell tools don’t match packaging builder | Align toolchain pins; see language toolchains |
.envrc blocked |
Security gate | Run direnv allow after reviewing .envrc |
Examples¶
flake.nix — default dev shell with packages and a shellHook. Illustrative; not evaluated in this vault. Needs nix-command and flakes:
{
description = "my-project dev shell";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.${system}.default = pkgs.mkShell {
packages = [
pkgs.git
pkgs.gnumake
# add language SDKs here — see language-toolchains.md in the wiki
];
shellHook = ''
echo "dev shell (${system}) — run make, git, …"
'';
};
};
}
.envrc — auto-enter with nix-direnv (assumes nix-direnv is installed or sourced from ~/.config/direnv/direnvrc):
Optional: pin nix-direnv inside the project when it is not global—see direnv / nix-direnv.
Activate and check:
# one-time: lock the nixpkgs input
nix flake update # or nix flake lock after editing inputs
# manual enter (experimental features required)
nix develop
# automatic enter (direnv + nix-direnv installed)
direnv allow
cd . # env loads; leaving unloads
# discover outputs
nix flake show
# sanity: tools on PATH inside the shell
which git && git --version
Classic alternative — no flake; shell.nix at repo root:
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
packages = [ pkgs.hello pkgs.git ];
shellHook = ''
echo "entered classic mkShell"
'';
}
Contrast nix shell (does not load this project’s mkShell):
References¶
- Nixpkgs manual —
pkgs.mkShell—packages,inputsFrom,shellHook - Nix manual —
nix develop— devShell resolution, experimentalnix-command - direnv — directory-scoped environment load/unload
- nix-community/nix-direnv — cached
use flake/use nix, GC roots viaprint-dev-env
See also¶
- Shells and direnv — dev shell concepts and CLI comparison
- Packages, apps, devShells — flake output layout for
devShells - direnv / nix-direnv — install, caching, GC roots
- nix build / develop / run —
nix developvsnix shellvsnix run - Language toolchains — SDKs inside
mkShell - Garbage collection — why nix-direnv registers GC roots
- nix-command · flakes — enabling experimental features
- Example corpus — hello-flake, shell.nix