Running nix develop in a freshly cloned repository fails with error: flake 'git+file:///home/dev/shop' does not provide attribute 'devShells.aarch64-darwin.default', or with error: getting status of '/nix/store/...-source/flake.nix': No such file or directory right after creating the file. Both are common first-hour failures when writing a flake, and both have precise causes. This page writes a working, multi-platform dev shell from scratch and pins exact tool versions, as part of reproducible dev shells with Nix, Devbox and direnv.

The end result is a flake.nix and flake.lock in the repository that give every developer on macOS or Linux, Intel or ARM, the same tools, with one command.

Diagnostic

Show what the flake exposes and whether Nix can see it at all:

#!/usr/bin/env bash
set -euo pipefail
nix --version
git status --short flake.nix flake.lock || true
nix flake show 2>&1 | head -12
nix eval --impure --raw --expr builtins.currentSystem; echo

Expected bad output for the two common failures:

nix (Nix) 2.24.6
?? flake.nix
error: getting status of '/nix/store/7lq...-source/flake.nix': No such file or directory
aarch64-darwin

flake.nix is untracked (??). And after git add, a flake written with only devShells.x86_64-linux.default shows no output for aarch64-darwin, which is what this machine is.

Reading the First nix develop Error Decision diagram mapping two common flake errors to their causes. Reading the First nix develop Error What does nix develop report? file not found in store flake.nix not git-added does not provide attribute no output for this system
Git visibility and per-system outputs cause most first-run flake failures.

Root cause

Flakes are evaluated from a copy of the repository in the Nix store, and for git repositories Nix copies only files that git knows about. An untracked flake.nix is invisible, so Nix reports it missing even though it is right there on disk. It does not need to be committed, only added to the index. The second failure comes from the structure of flake outputs: every output is keyed by a system string such as x86_64-linux, aarch64-linux, x86_64-darwin or aarch64-darwin. A shell declared for one system does not exist for the others, and Nix does not fall back. Helpers like flake-utils.lib.eachDefaultSystem generate the same shell for all four systems from one definition.

A third, quieter issue is version pinning. pkgs.nodejs_20 gives whatever Node 20 patch release is in the pinned nixpkgs revision. That is reproducible — everyone with the same lockfile gets the same patch — but it may not be the exact patch production runs. When an exact version matters, it has to come from a nixpkgs revision that contains it, or from an override.

Resolution

  1. Write the flake for all common systems:
{
  description = "shop dev shell";
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
    flake-utils.url = "github:numtide/flake-utils";
  };
  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };
        linuxOnly = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.strace pkgs.inotify-tools ];
      in {
        devShells.default = pkgs.mkShell {
          packages = with pkgs; [ nodejs_20 python312 jq terraform kubectl protobuf ] ++ linuxOnly;
          env.PIP_REQUIRE_VIRTUALENV = "true";
        };
      });
}

lib.optionals pkgs.stdenv.isLinux keeps Linux-only tools out of the macOS shell, where they do not build.

  1. Make it visible to Nix and generate the lockfile:
#!/usr/bin/env bash
set -euo pipefail
git add flake.nix
nix flake lock
git add flake.lock
nix flake show
  1. Pin an exact version when the patch level matters. Add a second nixpkgs input at a revision that contains the version, and take that one package from it. Tools such as the nixhub.io search show which nixpkgs revision provides a given version:
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
    nixpkgs-terraform.url = "github:NixOS/nixpkgs/5ed627539ac84809c78b2dd6d26a5cebeb5ae269";
    flake-utils.url = "github:numtide/flake-utils";
  };
  outputs = { self, nixpkgs, nixpkgs-terraform, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };
        tfpkgs = import nixpkgs-terraform { inherit system; config.allowUnfree = true; };
      in {
        devShells.default = pkgs.mkShell {
          packages = [ pkgs.nodejs_20 tfpkgs.terraform ];
        };
      });
}

allowUnfree is needed because Terraform's licence is not free in the nixpkgs sense; Nix refuses unfree packages unless explicitly allowed, which is a deliberate safety check rather than an error.

  1. Enter the shell and record the result:
#!/usr/bin/env bash
set -euo pipefail
nix develop --command bash -c 'node --version; terraform version | head -1; command -v protoc'
How nix develop Builds the Shell Flow from the git-tracked flake through lockfile resolution and the store to the interactive shell. How nix develop Builds the Shell git-tracked files copied to store flake.lock exact revisions system output this machine only shell tools on PATH
Only git-tracked files enter the evaluation, and only this system's output is used.

Expected output

$ nix flake show
git+file:///home/dev/shop
└───devShells
    ├───aarch64-darwin
    │   └───default: development environment 'nix-shell'
    ├───aarch64-linux
    │   └───default: development environment 'nix-shell'
    ├───x86_64-darwin
    │   └───default: development environment 'nix-shell'
    └───x86_64-linux
        └───default: development environment 'nix-shell'
$ nix develop --command bash -c 'node --version; terraform version | head -1'
v20.15.1
Terraform v1.8.5

Four systems are exposed, and the shell provides the pinned versions on this machine.

Run the same nix develop --command line on a colleague's machine with a different operating system. The version strings match, and on the same system the store paths printed by command -v match character for character, including the hash. That hash is derived from every input used to build the package — source, compiler, flags, dependencies — so matching hashes are a stronger guarantee than matching version numbers: two builds of "Node 20.15.1" compiled against different OpenSSL versions would have different hashes, and Nix would treat them as different packages.

The flake is also the right place for small environment conventions that otherwise live in personal shell configuration. env.PIP_REQUIRE_VIRTUALENV in the example stops anyone from installing Python packages into the Nix-provided interpreter by mistake, which would fail anyway because the store is read-only, but with a far less helpful error. Similar one-line settings — a GOFLAGS value, a NODE_OPTIONS memory limit — belong here too, so the shell behaves the same for everyone.

Prevention

  1. Check the flake in CI on both Linux and macOS runners with nix flake check and nix develop --command true. A package that fails to build on one platform is caught before a developer on that platform hits it.

  2. Schedule lockfile updates in a monthly pull request so nixpkgs security fixes arrive regularly and upgrades are reviewed, rather than happening whenever someone runs nix flake update by accident.

  3. Warn on a dirty tree only when it matters. Nix prints warning: Git tree is dirty whenever uncommitted changes exist; it is harmless during development. Do not suppress it globally — it is a useful reminder when debugging a shell that differs from CI.

Life of a Pinned Toolchain Timeline of a flake lockfile from creation through scheduled updates and a rollback. Life of a Pinned Toolchain Day 0 flake.lock created Month 1 update PR, CI green Month 2 update PR, one fix Month 3 bad update reverted Month 4 update PR merged
Every change to the toolchain is a reviewed lockfile diff with a clear rollback.

Platform caveats

macOS: after a macOS major upgrade, nix may disappear from PATH because the upgrade rewrote /etc/zshrc. Re-run the installer's repair command or re-add the Nix profile sourcing line; the store itself is intact.

Apple Silicon (ARM64): a few packages are not cached for aarch64-darwin and build from source on first use. If one is slow, check nix path-info --store https://cache.nixos.org <path>; if it is missing, cache it in a team cache after the first build.

WSL2: use the Linux instructions inside the distribution. Enable systemd in /etc/wsl.conf for the multi-user daemon, or use the single-user install.

Rollback

Revert the lockfile to return to the previous toolchain; old store paths are usually still present, so nothing needs to be downloaded:

#!/usr/bin/env bash
set -euo pipefail
git checkout HEAD~1 -- flake.lock
nix develop --command bash -c 'node --version'

Frequently Asked Questions

Why does Nix say flake.nix does not exist when it is in the directory?

In a git repository, Nix only sees files tracked by git. Run git add flake.nix (committing is not required) and try again.

How do I get an exact patch version of a tool?

Find a nixpkgs revision that contains it, add that revision as a separate input, and take the package from it. For languages with their own version files, such as .nvmrc, a tool like mise may be simpler for that one runtime.

Do I need flake-utils?

No, but without it you must write each system's output by hand or with nixpkgs.lib.genAttrs. flake-utils.lib.eachDefaultSystem keeps the file short and ensures all four common systems are covered.

Why does Nix refuse to install Terraform?

Terraform uses the Business Source License, which nixpkgs classifies as unfree. Set config.allowUnfree = true when importing the nixpkgs instance that provides it, or use OpenTofu, which is free and packaged as opentofu.