agentskills.codes
NI

nix-for-dev

Use this when setting up Nix for a development project (devShell + package build) and you care about `nix develop` being fast. Covers the zero-inputs flake.nix + npins + default.nix/shell.nix layout, sub-flakes for non-user-facing Nix, and language-specific recommendations.

Install

mkdir -p .claude/skills/nix-for-dev && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/14683" && unzip -o skill.zip -d .claude/skills/nix-for-dev && rm skill.zip

Installs to .claude/skills/nix-for-dev

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Use this when setting up Nix for a development project (devShell + package build) and you care about `nix develop` being fast. Covers the zero-inputs flake.nix + npins + default.nix/shell.nix layout, sub-flakes for non-user-facing Nix, and language-specific recommendations.
274 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)

About this skill

Nix for development

A low-overhead Nix setup for dev projects: ~1s cold nix develop, ~0.1s warm, with a clean separation between the user-facing flake and internal Nix. Reference implementation: juspay/kolu.

Core principle: zero flake inputs

The top-level flake.nix declares no inputs at all. Each flake input adds ~1.5s of fetcher-cache verification on cold eval; a single nixpkgs input costs ~7s. With zero inputs, cold nix develop is ~1.0s, warm ~0.1s.

Instead, pin sources with npins and import them via fetchTarball / callPackage from files under nix/.

# flake.nix — slim, zero inputs
{
  outputs = { self, ... }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
      eachSystem = f: builtins.listToAttrs (map
        (system: {
          name = system;
          value = f (import ./nix/nixpkgs.nix { inherit system; });
        })
        systems);
    in
    {
      packages = eachSystem (pkgs: {
        default = import ./default.nix { inherit pkgs; };
      });
      devShells = eachSystem (pkgs: {
        default = import ./shell.nix { inherit pkgs; };
      });
    };
}

Do not add nixpkgs, flake-parts, git-hooks, etc. as flake inputs. If a downstream consumer needs to override the pinned nixpkgs, they can override the npins source (NPINS_OVERRIDE_nixpkgs=/path).

File layout

flake.nix          # slim wrapper, zero inputs
default.nix        # main package(s), pkgs ? import ./nix/nixpkgs.nix { }
shell.nix          # devShell,        pkgs ? import ./nix/nixpkgs.nix { }
nix/
  nixpkgs.nix      # imports npins source + applies overlay
  overlay.nix      # injects leaf packages into pkgs
  env.nix          # env vars shared by build + devShell + wrapper
  packages/<name>/ # callPackage-style leaf packages
npins/
  default.nix      # generated by npins (do not edit)
  sources.json     # pinned source revisions

default.nix and shell.nix both accept pkgs ? import ./nix/nixpkgs.nix { } so they also work via plain nix-build / nix-shell, not just nix develop.

npins workflow

npins init                                   # creates npins/ and pins nixpkgs
npins add github nixos nixpkgs --branch nixpkgs-unstable
npins update                                 # update all pins
npins update nixpkgs                         # update one

nix/nixpkgs.nix:

# Pinned nixpkgs import — managed by npins.
# To update: npins update nixpkgs
let
  sources = import ../npins;
  nixpkgs = import sources.nixpkgs;
in
args: nixpkgs (args // {
  overlays = (args.overlays or [ ]) ++ [ (import ./overlay.nix) ];
})

Leaf packages via overlay

Pure callPackage-style packages live in nix/packages/<name>/default.nix and are auto-injected via nix/overlay.nix:

# nix/overlay.nix
final: _prev: {
  my-fonts = final.callPackage ./packages/fonts { };
}

Packages that need per-invocation arguments (commit hash, build-time env) stay in the top-level default.nix — overlays are for things that legitimately belong on pkgs.

Shared env vars

Define a single nix/env.nix returning an attrset; both the build derivation and the devShell spread it into their env. This prevents drift between nix build and nix develop.

# nix/env.nix
{ pkgs }: {
  MY_FONTS_DIR = pkgs.my-fonts;
  MY_GH_BIN    = "${pkgs.gh}/bin/gh";
}

devShell conventions

  • Use pkgs.mkShell directly. Do not introduce flake-parts to "structure" it.
  • Use pkgs.writeShellApplication (not writeShellScriptBin) — strict mode + runtimeInputs validation. Always set meta.description.
  • Run nixpkgs-fmt from the devShell rather than wiring up formatter perSystem.
  • Expose extra shells via overrideAttrs so the base stays fast:
devShells = eachSystem (pkgs:
  let default = import ./shell.nix { inherit pkgs; };
  in {
    inherit default;
    e2e = default.overrideAttrs (prev: {
      env = (prev.env or { }) // {
        PLAYWRIGHT_BROWSERS_PATH = pkgs.playwright-driver.browsers;
      };
    });
  });

nix develop .#e2e for the heavier shell; the default stays cold-start-fast.

Sub-flakes for non-user-facing Nix

Module integration tests (home-manager, NixOS, Darwin) genuinely need flake-parts-style inputs (home-manager, nix-darwin). Keep those inputs out of the top-level flake by nesting a sub-flake under nix/<name>/flake.nix. CI builds it with --override-input pointing back at the parent:

# nix/home/example/flake.nix
{
  inputs = {
    self_pkg.url     = "github:owner/repo";   # parent; CI passes --override-input
    nixpkgs.url      = "github:nixos/nixpkgs/nixpkgs-unstable";
    home-manager.url = "github:nix-community/home-manager";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";
  };
  outputs = { nixpkgs, home-manager, self_pkg, ... }: {
    # nixosConfigurations / checks that exercise self_pkg.homeManagerModules.default
  };
}

Users running nix develop / nix run on the top-level flake never evaluate this graph. Only CI does.

Language templates

Haskell

Use haskell-flake via its standalone entry point lib.evalHaskellProject (not the flake-parts module). See haskell.nixos.asia/standalone for the full API.

In keeping with the zero-inputs principle, pin haskell-flake via npins (npins add github srid haskell-flake) and call it from default.nix:

# default.nix
{ pkgs ? import ./nix/nixpkgs.nix { } }:
let
  sources = import ./npins;
  haskell-flake = import sources.haskell-flake;
  project = (haskell-flake.lib { inherit pkgs; }).evalHaskellProject {
    projectRoot = ./.;
    modules = [{
      settings.mypackage.haddock = false;
      devShell.tools = hp: { inherit (hp) fourmolu; };
    }];
  };
in
project.packages.mypackage.package

Wire project.devShell into shell.nix the same way. If you need flake-parts and nixos-unified autowiring (multi-package projects, fully wired checks), see the nix-haskell skill — it uses haskell-template and trades startup time for ergonomics.

TypeScript / pnpm

See nix-typescript for fetchPnpmDeps and hash management.

Dev services

For multi-process dev environments (server + watcher + db), use process-compose-flake and services-flake via their standalone entry points (no flake-parts needed). Both flakes have zero inputs themselves, so pinning them via npins keeps the top-level flake.nix zero-input too:

  • process-compose-flake exposes lib.evalModules / lib.makeProcessCompose for module evaluation outside flake-parts.
  • services-flake exposes processComposeModules.default (a path) — pass it as a module to evalModules.
# shell.nix
{ pkgs ? import ./nix/nixpkgs.nix { } }:
let
  sources = import ./npins;
  pcLib = import "${sources.process-compose-flake}/nix/lib.nix" { inherit pkgs; };
  servicesMod = pcLib.evalModules {
    modules = [
      "${sources.services-flake}/nix/process-compose"
      { services.redis."r1".enable = true; }
    ];
  };
in
pkgs.mkShell {
  inputsFrom = [ servicesMod.config.services.outputs.devShell ];
}

Reference: services-flake/example/without-flake-parts and doc/without-flake-parts.md.

Companion docs

  • nix-perf — diagnosing slow nix develop / nix flake archive
  • nix-justfile — justfile recipe conventions for Nix projects
  • nix-typescript — pnpm + Nix conventions
  • nix-haskell — flake-parts + haskell-template variant (when ergonomics > cold start)
  • juspay/kolu — full reference implementation

Search skills

Search the agent skills registry