Nix Dev Environments skill for Claude Code

One command, the exact tools CI uses. Claude inventories the tool versions a project really uses, then writes a flake.nix devShell for every platform the team runs, pinned by a committed flake.lock and activated automatically with nix-direnv. CI enters the same shell with a binary cache, application dependencies stay in their own lockfiles, and teams that do not want full Nix get devbox, devenv or mise instead.

  • namenix-environments
  • size241 lines · ≈2,100 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Builds reproducible per-project development environments with Nix flakes - flake.nix devShells for multiple systems, pinned nixpkgs via flake.lock, nix develop and nix-direnv auto-activation, language toolchains and native libraries, services such as Postgres in the shell, CI using the same shell with a binary cache - and chooses lighter alternatives (devbox, devenv, mise) when full Nix is too much. Use when setting up flake.nix or shell.nix, when "works on my machine" comes from tool version drift, when onboarding needs one command, or when the user mentions Nix, NixOS, direnv, devbox, devenv or mise.

Try asking

  • Create a flake.nix dev shell with Node 22, Python 3.12 and Postgres 16 for this repo
  • Onboarding takes a day because everyone has different tool versions. Fix it with Nix
  • We want pinned tools but not the Nix language. Should we use devbox or mise?

Use it when

  • Setting up flake.nix, shell.nix, devbox.json, devenv.nix or mise.toml
  • Bugs come from tool or runtime version drift between machines
  • CI and local environments should share exactly the same toolchain
  • A project needs native libraries that are painful to install by hand

Skip it when

How to check the result

A fresh clone runs the test command through nix develop on Linux and macOS, nix flake check passes, and tools inside the shell resolve to /nix/store paths.

The common mistake it prevents

Creating flake.nix without git add, so the flake cannot see it or its imported files and fails with confusing path-does-not-exist errors.

~/.claude/skills/nix-environments/SKILL.md

--- name: nix-environments description: Builds reproducible per-project development environments with Nix flakes - flake.nix devShells for multiple systems, pinned nixpkgs via… license: MIT ---

A dev environment is done when a new contributor runs one command (nix develop, or just cd with direnv) and gets the exact tool versions CI uses, on Linux and macOS, with the versions pinned in a committed lockfile and updated deliberately.

Ground rules

  • Pin through flake.lock and commit it. Update inputs on purpose (nix flake update) in their own PR, never as a side effect.
  • Flakes only see files tracked by Git. git add flake.nix flake.lock before the first nix develop, or evaluation fails with “path does not exist”.
  • Put tools in the shell, keep application dependencies in the language’s own lockfile (package-lock, uv.lock, go.sum). Do not repackage every npm/PyPI dependency in Nix unless you are building a Nix package for deployment.
  • Support every system the team uses (x86_64-linux, aarch64-linux, aarch64-darwin, x86_64-darwin if Intel Macs remain).
  • Garbage collection (nix-collect-garbage -d) deletes old generations and rollback points; ask before running it on someone’s machine.

Choose the tool

SituationChoose
Team accepts Nix; needs native libs (openssl, libpq, imagemagick) and exact pinsNix flake devShell (default)
Wants Nix pinning without learning the language; JSON configdevbox (devbox.json, Nix underneath)
Wants services (Postgres, Redis), processes and pre-commit hooks declared togetherdevenv (devenv.nix)
Only needs language runtimes (node, python, go, terraform) and task runningmise (mise.toml)
Windows developers without WSLmise or devcontainers; Nix requires Linux/macOS/WSL
Needs a full isolated OS imageDevcontainer (can still use Nix inside)

Workflow

- [ ] 1. Inventory tools and versions in use (CI config, READMEs, .tool-versions, Dockerfiles)
- [ ] 2. Install Nix with flakes enabled
- [ ] 3. Write flake.nix with a multi-system devShell
- [ ] 4. Add direnv auto-activation
- [ ] 5. Wire CI to the same shell with caching
- [ ] 6. Verify on a clean checkout and a second OS; document

1. Inventory

cat .tool-versions .nvmrc .python-version 2>/dev/null
grep -rhoE "(node-version|python-version|go-version):.*" .github/workflows/ | sort -u
grep -E "^FROM" Dockerfile* 2>/dev/null

Make a list: runtime versions, CLIs (jq, terraform, kubectl), native libraries, and local services.

2. Install

Use the Determinate Systems installer (enables flakes, supports clean uninstall) or the official multi-user installer and then enable flakes:

# official installer path: enable flakes afterwards
mkdir -p ~/.config/nix
echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
nix --version

3. flake.nix

{
  description = "acme-api development environment";

  inputs = {
    # use the current stable release branch (e.g. nixos-25.11) or nixpkgs-unstable
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
  };

  outputs = { self, nixpkgs }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];
      forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f (import nixpkgs {
        inherit system;
        config.allowUnfree = false;   # set true only for tools like terraform (BSL)
      }));
    in {
      devShells = forAllSystems (pkgs: {
        default = pkgs.mkShell {
          packages = with pkgs; [
            nodejs_22
            pnpm
            python312
            uv
            postgresql_16
            jq
            just
            openssl
            pkg-config
          ];

          env = {
            PGDATA = "./.pgdata";
            UV_PYTHON_DOWNLOADS = "never";   # make uv use the Nix python
          };

          shellHook = ''
            export PATH="$PWD/node_modules/.bin:$PATH"
            echo "node $(node --version), python $(python3 --version | cut -d' ' -f2)"
          '';
        };
      });

      formatter = forAllSystems (pkgs: pkgs.nixfmt-rfc-style);
    };
}

Find attribute names and versions: nix search nixpkgs postgresql or search.nixos.org. Versioned attributes (nodejs_22, python312, postgresql_16) are stable choices; unversioned ones (nodejs, go) move with nixpkgs updates.

Need an exact older version not in the pinned branch: add a second input pinned to a specific nixpkgs commit (find it via a version history site such as nixhub.io or lazamar’s nix-versions) and take only that package from it:

inputs.nixpkgs-tf.url = "github:NixOS/nixpkgs/<commit-sha>";
# outputs = { self, nixpkgs, nixpkgs-tf }: ... packages = [ nixpkgs-tf.legacyPackages.${system}.terraform ];

Multiple shells for different tasks: devShells.<system>.ci, used with nix develop .#ci.

git add flake.nix && nix flake lock && git add flake.lock
nix develop                       # enter the shell
nix develop --command pnpm test   # run one command inside it
nix flake check                   # evaluate outputs for all systems
nix flake show
nix flake update                  # bump all inputs (own PR)
nix flake update nixpkgs          # bump one input

Local services inside the shell: for Postgres, initdb into $PGDATA once and pg_ctl -l .pgdata/log start; add .pgdata/ to .gitignore. For several services, devenv or process-compose (services-flake) is cleaner than hand-written shellHooks.

4. direnv

# .envrc
use flake
dotenv_if_exists .env.local

Install direnv and nix-direnv (caches the shell so cd is instant and the environment survives garbage collection), then direnv allow. Add .direnv/ to .gitignore.

5. CI

# GitHub Actions
- uses: actions/checkout@v5
- uses: DeterminateSystems/nix-installer-action@main   # pin to a SHA in real workflows
- uses: cachix/cachix-action@v15                        # or another binary cache
  with: { name: acme, authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" }
- run: nix develop --command just ci

Without a binary cache, CI rebuilds or re-downloads the closure each run; most packages come from cache.nixos.org, so only custom derivations need your own cache (Cachix, FlakeHub Cache, or self-hosted Attic).

Lighter alternatives

devbox:

devbox init
devbox add nodejs@22 [email protected] postgresql@16
devbox shell
devbox generate direnv            # writes .envrc

mise (mise.toml):

[tools]
node = "22"
python = "3.12"
terraform = "1.9"

[tasks.test]
run = "pnpm test"
mise install && mise run test

devenv: devenv init, then declare languages.javascript.enable = true;, services.postgres.enable = true; in devenv.nix and run devenv up for services.

6. Verify

  • Fresh clone in a temp directory: nix develop --command <test command> succeeds.
  • Run on a second platform (macOS and Linux) or CI matrix with both.
  • which node inside the shell points into /nix/store/....
  • README “Getting started” reduced to: install Nix + direnv, direnv allow.

Gotchas

  • Untracked files are invisible to flakes; a new shell.nix import or script that is not git add-ed causes confusing “does not exist” errors.
  • Prebuilt Python wheels and Node native modules on NixOS can fail to find shared libraries (libstdc++.so.6). Add the library to the shell and expose it, or prefer building from source; on macOS and non-NixOS Linux this rarely appears.
  • allowUnfree is needed for packages under non-free licenses (e.g. Terraform BSL releases); OpenTofu avoids it.
  • Darwin and Linux package sets differ; some packages are Linux-only. Guard with pkgs.lib.optionals pkgs.stdenv.isLinux [ ... ].
  • nix-shell and shell.nix (legacy) do not use flake.lock; if both exist, keep one source of truth or make shell.nix call the flake via flake-compat.
  • Large closures slow first entry; nix-direnv’s cache and a binary cache fix repeat costs, not the first download.
  • shellHook runs on every entry; keep it fast and idempotent (no installs there).

Output

**Tool:** Nix flake devShell + nix-direnv
**Pinned:** nixpkgs nixos-25.11 @ <rev> (flake.lock committed)
**Shell:** node 22, pnpm, python 3.12 + uv, postgres 16, jq, just, openssl
**Systems:** x86_64-linux, aarch64-linux, aarch64-darwin (nix flake check passes)
**CI:** nix-installer-action + Cachix; `nix develop --command just ci`
**Onboarding:** install Nix + direnv, `direnv allow` (README updated)

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc