Short answer: use Nix + direnv (nix-direnv) + flakes.
Result: when you cd into a project, your shell auto-loads the exact tools for that repo; when you delete the folder, its env stops existing and the packages get garbage-collected. No more “uhh which package manager did I use for this?”
Here’s the playbook—fast, opinionated, and minimal chaos.
# Nix (multi-user). This is the official script; it's fine.
sh <(curl -L https://nixos.org/nix/install) --daemon# direnv + nix integration (Arch repos)
sudo pacman -S direnv
# Add direnv to your shell (zsh for you)
nvim ~/.zshrcPaste this into ~/.zshrc and save:
# --- direnv hook (auto-load per-directory envs) ---
eval "$(direnv hook zsh)"Then:
# Enable nix-direnv integration (lets direnv understand flakes)
nix-env -iA nixpkgs.nix-direnv# Allow direnv in this directory (one-time per dir):
direnv allownvim .envrc:
# Use nix flakes to define the dev shell for this project.
# This automatically loads when you `cd` here and unloads when you leave.
use flakenvim flake.nix:
{
description = "Per-project dev environment (reproducible, auto-loaded)";
inputs = {
# Nixpkgs channel. Pin a known-good revision if you want.
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
# Optional: devshell helpers
devshell.url = "github:numtide/devshell";
};
outputs = { self, nixpkgs, devshell }:
let
# Pick your system; change if you're on aarch64, etc.
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
# Optional: allow unfree if you need proprietary stuff
# config.allowUnfree = true;
};
in
{
# `nix develop` or `direnv` will enter this shell.
devShells.${system}.default = pkgs.mkShell {
# --- Shell-wide env vars (project-specific) ---
# e.g., export DATABASE_URL="postgres://..."
# NIX_CFLAGS_COMPILE = "-O2 -pipe";
# --- Tools you want available exactly in this project ---
buildInputs = [
# Core CLIs
pkgs.git
pkgs.curl
pkgs.just # task runner (nice to have)
# Languages / Toolchains
pkgs.nodejs_20
pkgs.pnpm
pkgs.python312
pkgs.poetry
pkgs.go
pkgs.rustup
pkgs.pkg-config
# Common libs for building (adjust per project)
pkgs.openssl
pkgs.zlib
pkgs.cmake
pkgs.gnumake
];
# --- Post-activation hooks (runs on shell enter) ---
shellHook = ''
# Keep it obvious you’re in the project env
echo "[nix] dev env activated for ${PWD##*/}"
# Node: enable Corepack if you lean yarn/pnpm lockfiles
if command -v corepack >/dev/null 2>&1; then
corepack enable >/dev/null 2>&1 || true
fi
# Rust toolchain per project (optional)
rustup default stable >/dev/null 2>&1 || true
'';
};
};
}How this feels day-to-day:
cd my-repo→ direnv detects.envrc→ loads flake dev shell → all tools in PATH.cd ..→ it unloads. No host pollution, no “which package manager did Past You use?”
- Nix stores project env builds in the nix store but they’re only kept alive by GC roots (direnv/nix-direnv makes ephemeral roots under
~/.cache/nix/direnv). - Delete the project directory → you’ve effectively removed its root.
- Run garbage collection to reclaim space:
# Manual GC (reclaim dead packages & generations)
nix-collect-garbage --delete-old
# Also clean old direnv shells
nix-direnv pruneCreate a weekly GC timer so you never think about it again.
nvim ~/.config/systemd/user/nix-gc.service:
[Unit]
Description=Nix Garbage Collection (user)
[Service]
Type=oneshot
ExecStart=/usr/bin/env nix-collect-garbage --delete-oldnvim ~/.config/systemd/user/nix-gc.timer:
[Unit]
Description=Weekly Nix GC (user)
[Timer]
OnCalendar=weekly
Persistent=true
[Install]
WantedBy=timers.targetEnable it:
systemctl --user daemon-reload
systemctl --user enable --now nix-gc.timerNow when you trash a repo, its env will get collected automatically on the next run.
- Per-repo language managers still work inside the dev shell, but try to keep the shell authoritative (pin versions here).
- Lock the world: keep
flake.lockin git so teammates get identical tools. - Project bootstrap: add a
justfile:
nvim justfile:
# Comments because Future You is forgetful.
default: # Show help
just --list
setup: # First-time project setup
# e.g., pnpm install or poetry install; runs inside the nix shell
pnpm install
poetry install --no-rootCreate a tiny template repo you can copy:
gh repo create devshell-template --private
# Drop your flake.nix, .envrc, justfile in thereThen for any new project:
cp -r ~/code/devshell-template new-project
cd new-project
direnv allow- When? Every time you
cdinto a project (thanks todirenv). - How? Put a
flake.nix(orshell.nix) +.envrcin the repo. - Cleanup? Delete the project → run GC (or let your systemd timer do it) → packages are gone.