Skip to content
cccskills
FreePublished on GitHub

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.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md8.4 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

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

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

be

Free

Modern, interactive alternative to `/do` — clarify intent up front, then take a task end-to-end with a serial AI review gauntlet (lens debate (lowy ⇄ hickey) → codex debate → simplify → code-police, each editing the branch in turn) → CI → evidence. ONLY invoke when the user explicitly types `/be` or `$be`; never auto-select from a natural-language request.

The English summary is being prepared. Showing the original description.

srid/emanote966Updated Jul 26, 2026

Run /be's review gauntlet SERIALLY — /lens-debate (lowy ⇄ hickey), then /codex-debate, then /simplify, then code-police, each editing and committing on the live branch in turn. Use from /be §4, or when the user asks to "run the review gauntlet". Requires Claude Code's Skill tool.

The English summary is being prepared. Showing the original description.

srid/emanote966Updated Jul 26, 2026

ci

Free

Reference for the `odu` runner — how to invoke a full pipeline, a single recipe, or a platform-pinned node, and how to attach to a live run, from a project whose CI odu runs. Trigger when the user asks to "run CI", "run the pipeline", "re-run a check", to run named lanes or recipes (e.g. "run fmt and nix", "just the e2e lane", bare selectors like `fmt`/`nix`/`e2e`), or names a recipe by `<recipe>@<platform>`. This skill — not a repo's local `just ci` / `just <recipe>` — is how an odu-run request is served.

The English summary is being prepared. Showing the original description.

srid/emanote966Updated Jul 26, 2026

Review code for quality, simplicity, and common mistakes before declaring work complete.

The English summary is being prepared. Showing the original description.

srid/emanote966Updated Jul 26, 2026

Run an automated codex⇄Claude debate to consensus — no round cap, no deadlock exit. Two explicit subcommands. `review` (also the bare/back-compat default) — codex (reviewer) critiques the current diff and a Claude subagent (author) fixes/disputes, looping until they agree. `answer` — Claude and codex each answer a freeform prompt in parallel, then cross-check until they agree, and a unified answer is returned. Use when the user types `/codex-debate`, asks to "have codex review this", "run the codex debate", "review this PR with codex", "argue this with codex until you agree", or passes a question to "have Claude and codex debate/answer until they agree".

The English summary is being prepared. Showing the original description.

srid/emanote966Updated Jul 26, 2026

do

Free

Do a task end-to-end — implement, PR, CI loop, ship. ONLY invoke when the user explicitly types `/do` or `$do`; never auto-select from a natural-language request, even one that sounds like an end-to-end task.

The English summary is being prepared. Showing the original description.

srid/emanote966Updated Jul 26, 2026

See all skills by srid

Report a problem with this skill