Day 28 — Dev shells & direnv
Day 28 — Dev shells & direnv
Stage III · ~4h
Goal: Keep project toolchains in flake devShells, load them automatically with direnv (use flake), and stop stuffing every language toolchain into system or HM profiles.
Why this day exists
Two failure modes dominate Nix workstations:
- Profile bloat: Go, Node, Rust, Python, JDK all in
home.packagesforever
- Snowball shells: random
nix-shell -pwith no lock, unreproducible on Monday
The modern fix:
| Layer | Holds |
|---|---|
| NixOS / HM | OS, editors, generic CLI, user services |
Project flake devShells |
Compilers, linters, project CLIs |
| direnv | Enters/exits the shell when you cd |
This matches Day 8’s first project flake—and now integrates with the Stage III host.
Theory 1 — devShells in a flake
Minimal project flake (can be separate from the NixOS flake):
# ~/lab/demo-app/flake.nix
{
description = "demo app toolchain";
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 = with pkgs; [
go_1_24 # adjust to versions available on your pin
gopls
git
];
shellHook = ''
export DEMO_APP=1
echo "entered demo-app devShell"
'';
};
};
}Enter manually:
cd ~/lab/demo-app
nix developWhy not always put shells in the NixOS flake?
You can expose devShells from the host flake, but project repos usually own their own flake so clones on other machines work without your entire OS config. Host flake remains for nixosConfigurations (+ later checks).
Theory 2 — direnv + nix-direnv
direnv loads environment variables when you enter a directory with .envrc.
nix-direnv (recommended) caches Nix flake shells efficiently so every cd is not a full re-eval disaster.
HM installation pattern
# homes/alice/direnv.nix
{ pkgs, ... }:
{
programs.direnv = {
enable = true;
nix-direnv.enable = true;
enableBashIntegration = true;
# enableZshIntegration / fish as needed
};
}Rebuild host/HM, then new shells pick up the hook (eval "$(direnv hook bash)" is usually wired by the module).
Theory 3 — .envrc with use flake
In the project:
# ~/lab/demo-app/.envrc
use flakeAllow once:
direnv allowBehavior:
| Action | Result |
|---|---|
cd into project |
Loads devShell env |
cd out |
Unloads |
| flake/lock change | direnv reloads (may rebuild) |
.envrc is not for secrets
Do not put API tokens in .envrc committed to git. Use:
- Untracked
.envrc.localpatterns (with care)
- Secret managers / sops (Stage IV)
- Runtime
direnvprivate files excluded by git
.envrc.local
.env
Theory 4 — System packages vs project shells
| Put in HM/system | Put in devShell |
|---|---|
git, neovim, htop |
go, cargo, node, python venv tools |
| direnv itself | project linters pinned to repo |
| SSH client | DB client only for one service’s repo |
Test: if two projects need different major versions, it belongs in the project shell.
Theory 5 — Pinning discipline for project flakes
Project flake.lock should be committed for apps you ship or share. For private throwaways, still locking saves future you.
Update project nixpkgs independently of the host flake—on purpose. Host OS pin ≠ every app pin.
Theory 6 — nix develop vs nix shell vs HM packages
| Command | Role |
|---|---|
nix develop |
Flake devShell (hooks, packages, inputsFrom) |
nix shell nixpkgs#foo |
Ad-hoc package on PATH |
home.packages |
Always-on user profile |
direnv automates nix develop-class environments.
Worked example — Multi-tool shell
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
python3
python3Packages.pytest
ruff
];
shellHook = ''
export PYTHONPATH="$PWD/src''${PYTHONPATH:+:$PYTHONPATH}"
'';
};# .envrc
use flakeLab 1 — Install direnv via HM
Add programs.direnv with nix-direnv.enable = true. Rebuild. Verify:
command -v direnv
direnv versionOpen a new login shell if hooks are missing.
Lab 2 — Create a project flake
mkdir -p ~/lab/90daysofx/02-nixos/day28/demo-app
cd ~/lab/90daysofx/02-nixos/day28/demo-app
# write flake.nix with mkShell and 2–3 tools
nix flake lock
nix develop -c which gitLab 3 — Wire direnv
echo 'use flake' > .envrc
direnv allow
cd ..
cd demo-app
echo "DEMO or PATH check: $PATH" | head -c 200; echo
type python3 2>/dev/null || type go 2>/dev/null || trueConfirm leaving the directory drops the env (compare which before/after).
Lab 4 — Prove version isolation
If available, make two tiny projects with different tools (e.g. one with hello only, one with cowsay only—or different language tools). cd between them; show different PATH entries.
Lab 5 — Thin the profile (careful)
List home.packages / system packages. Move one language toolchain from always-on profile into a project shell. Rebuild HM. Confirm the tool is gone from bare shells but present under direnv.
Do not remove your editor mid-day without a plan.
Lab 6 — Document the workflow
Journal template:
cd project → direnv loads flake lock pin → tools available
change flake → direnv reload → maybe download/build
secrets → never in .envrc committed file
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| direnv not hooking | New shell; enable integration for your shell |
use flake blocked |
direnv allow |
Slow every cd |
Ensure nix-direnv enabled |
| Wrong system arch in flake | Match system to the machine |
Secrets in .envrc committed |
Rotate; use gitignored local files / sops |
| Host flake-only shells | Fine for mono-lab; projects should still travel |
shellHook prints annoyingly |
Keep hooks quiet in shared repos |
Checkpoint
- direnv + nix-direnv via HM
- At least one project with
devShells.default+ lock
.envrcwithuse flakeallowed
- Enter/leave directory toggles tools
- One toolchain removed from always-on profile (or plan written)
- Secret policy for env files stated
Commit
# host flake
git add .
git commit -m "day28: direnv and project devShell workflow"
# also commit inside demo-app if it is its own git repoWrite three personal gotchas before continuing.
Tomorrow
Day 29 — flake-parts or discipline: reduce multi-output flake boilerplate—or refuse the dependency and keep a clean hand-rolled structure. Pick one house style.