Skip to content
Back to skills

Nix For Dev

ASecurity

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.

  • 11 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 6, 2026
testingtypescriptgoshellbashgitapi

Works with

  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned October 6, 2026

npx -y skills add juspay/skills --skill nix-for-dev --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Nix For Dev?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Nix For Dev
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/juspay-nix-for-dev/badge)](https://www.skillsdirectory.com/skills/juspay-nix-for-dev)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: nix-for-dev
description: 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.
---

# 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](https://github.com/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](https://github.com/andir/npins) and import them via `fetchTarball` / `callPackage` from files under `nix/`.

```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

```bash
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`:

```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
# 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
# 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:

```nix
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
# 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](https://github.com/srid/haskell-flake) via its **standalone** entry point `lib.evalHaskellProject` (not the flake-parts module). See [haskell.nixos.asia/standalone](https://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`:

```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](https://github.com/srid/haskell-template) and trades startup time for ergonomics.

### TypeScript / pnpm

See [`nix-typescript`](../nix-typescript/SKILL.md) for `fetchPnpmDeps` and hash management.

### Dev services

For multi-process dev environments (server + watcher + db), use [`process-compose-flake`](https://github.com/Platonic-Systems/process-compose-flake) and [`services-flake`](https://github.com/juspay/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`](https://github.com/Platonic-Systems/process-compose-flake/blob/main/nix/lib.nix) for module evaluation outside flake-parts.
- `services-flake` exposes `processComposeModules.default` (a path) — pass it as a module to `evalModules`.

```nix
# 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`](https://github.com/juspay/services-flake/tree/main/example/without-flake-parts) and [`doc/without-flake-parts.md`](https://github.com/juspay/services-flake/blob/main/doc/without-flake-parts.md).

## Looking things up

When the `nixos` MCP server is available, use it to look up package names, versions, and NixOS / Home Manager options instead of guessing them.

## Companion docs

- [`nix-perf`](../nix-perf/SKILL.md) — diagnosing slow `nix develop` / `nix flake archive`
- [`nix-justfile`](../nix-justfile/SKILL.md) — justfile recipe conventions for Nix projects
- [`nix-typescript`](../nix-typescript/SKILL.md) — pnpm + Nix conventions
- [`nix-haskell`](../nix-haskell/SKILL.md) — flake-parts + haskell-template variant (when ergonomics > cold start)
- [juspay/kolu](https://github.com/juspay/kolu) — full reference implementation

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…