Nix builds packages and environments reproducibly — given the same inputs, it always produces the same output. Nix flakes make this reproducibility shareable: your flake.nix pins exact versions of every dependency, and any developer running nix develop gets the exact same shell regardless of their OS or existing tooling. Claude Code writes flake configurations, explains the Nix expression language, and sets up development shells, CI environments, and NixOS modules.
CLAUDE.md for Nix Projects
## Nix Configuration
- Nix flakes enabled (nix.settings.experimental-features = "nix-command flakes")
- nixpkgs: follows input pinned in flake.lock — never use `<nixpkgs>`
- devShell: defined in flake.nix — all developers use `nix develop`
- Packages: prefer nixpkgs versions; custom derivations in ./nix/
- CI: nix build + nix flake check (runs all checks in the flake)
- NixOS modules: declared in ./modules/, imported in flake.nix
- Never use nix-env -i globally — all tooling in devShell or home-manager
Flake.nix for a Development Project
# flake.nix
{
description = "Order management service";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
flake-utils.url = "github:numtide/flake-utils";
# Rust toolchain pinning
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, flake-utils, fenix }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
# Pinned Rust toolchain (from rust-toolchain.toml)
rustToolchain = fenix.packages.${system}.fromToolchainFile {
file = ./rust-toolchain.toml;
sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
in {
# `nix develop` drops you here
devShells.default = pkgs.mkShell {
buildInputs = [
# Language runtimes
pkgs.nodejs_20
pkgs.python311
rustToolchain
# Database tools
pkgs.postgresql_16
pkgs.redis
# Build tools
pkgs.gnumake
pkgs.gcc
pkgs.pkg-config
# Dev tooling
pkgs.git
pkgs.jq
pkgs.curl
pkgs.httpie
# Project-specific
pkgs.nodePackages.typescript
pkgs.nodePackages.prettier
];
# Environment variables in the dev shell
shellHook = ''
export DATABASE_URL="postgresql://localhost/orders_dev"
export REDIS_URL="redis://localhost:6379"
# Start local services if not running
if ! pg_isready -q; then
echo "Starting PostgreSQL..."
pg_ctl start -D ~/.postgres/data -l ~/.postgres/log
fi
echo "Dev environment ready. Run 'make dev' to start the server."
'';
};
# `nix build` builds the application
packages.default = pkgs.stdenv.mkDerivation {
pname = "order-api";
version = "0.1.0";
src = ./.;
buildInputs = [ pkgs.nodejs_20 ];
buildPhase = ''
npm ci
npm run build
'';
installPhase = ''
mkdir -p $out/lib/order-api
cp -r dist $out/lib/order-api/
cp package.json $out/lib/order-api/
mkdir -p $out/bin
cat > $out/bin/order-api << EOF
#!/bin/sh
exec ${pkgs.nodejs_20}/bin/node $out/lib/order-api/dist/server.js "\$@"
EOF
chmod +x $out/bin/order-api
'';
};
# `nix flake check` runs all these
checks = {
# Run tests
tests = pkgs.runCommand "tests" { buildInputs = [ pkgs.nodejs_20 ]; } ''
cd ${self}
npm ci
npm test
touch $out
'';
# Lint
lint = pkgs.runCommand "lint" { buildInputs = [ pkgs.nodejs_20 ]; } ''
cd ${self}
npm run lint
touch $out
'';
};
}
);
}
Custom Derivation (Package)
# nix/my-custom-tool.nix — package a Go binary not in nixpkgs
{ lib, buildGoModule, fetchFromGitHub }:
buildGoModule rec {
pname = "my-codegen";
version = "0.4.2";
src = fetchFromGitHub {
owner = "myorg";
repo = "codegen";
rev = "v${version}";
sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
vendorHash = "sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";
meta = with lib; {
description = "Code generator for myorg";
license = licenses.mit;
maintainers = [ maintainers.yourname ];
};
}
# Import in flake.nix
let
myCustomTool = pkgs.callPackage ./nix/my-custom-tool.nix {};
in pkgs.mkShell {
buildInputs = [ myCustomTool ] ++ existingTools;
}
Home Manager Module
# home-manager/modules/development.nix
{ config, pkgs, lib, ... }:
{
options.programs.devtools = {
enable = lib.mkEnableOption "developer tools";
extraPackages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [];
description = "Additional packages to install";
};
};
config = lib.mkIf config.programs.devtools.enable {
home.packages = with pkgs; [
git
gh
neovim
tmux
ripgrep
fd
bat
eza
delta # Better git diff
lazygit
] ++ config.programs.devtools.extraPackages;
programs.git = {
enable = true;
userName = "Your Name";
userEmail = "[email protected]";
extraConfig = {
core.pager = "${pkgs.delta}/bin/delta";
interactive.diffFilter = "${pkgs.delta}/bin/delta --color-only";
push.autoSetupRemote = true;
pull.rebase = true;
};
};
programs.zsh = {
enable = true;
enableCompletion = true;
initExtra = ''
# Load direnv for per-directory env vars
eval "$(${pkgs.direnv}/bin/direnv hook zsh)"
'';
};
};
}
NixOS Module
# modules/order-api.nix — systemd service for the order API
{ config, lib, pkgs, ... }:
let
cfg = config.services.order-api;
in {
options.services.order-api = {
enable = lib.mkEnableOption "order API service";
port = lib.mkOption {
type = lib.types.port;
default = 3000;
description = "Port to listen on";
};
environmentFile = lib.mkOption {
type = lib.types.path;
description = "Path to .env file with secrets";
};
};
config = lib.mkIf cfg.enable {
systemd.services.order-api = {
description = "Order API Service";
wantedBy = [ "multi-user.target" ];
after = [ "postgresql.service" "redis.service" "network.target" ];
environment = {
PORT = toString cfg.port;
NODE_ENV = "production";
};
serviceConfig = {
Type = "simple";
User = "order-api";
Group = "order-api";
ExecStart = "${pkgs.order-api}/bin/order-api";
EnvironmentFile = cfg.environmentFile;
# Security hardening
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
Restart = "on-failure";
RestartSec = "5s";
};
};
users.users.order-api = {
isSystemUser = true;
group = "order-api";
};
users.groups.order-api = {};
networking.firewall.allowedTCPPorts = [ cfg.port ];
};
}
direnv Integration
# .envrc — use in any project directory for auto-activation
# nix flake is loaded automatically when you cd into the directory
use flake
# Or for non-flake projects
use nix shell.nix
# Workflow
cd /path/to/project # direnv activates the nix devShell automatically
# All tools from flake.nix are now in PATH
# First time: allow the .envrc
direnv allow
# Update the flake lock (pin to latest nixpkgs)
nix flake update
# Run all checks before commit
nix flake check
# Build the deployment artifact
nix build
./result/bin/order-api --version
For the CI/CD integration that uses nix flake check for reproducible test runs, see the zero-downtime deployments guide. For supply chain security, the supply chain security guide covers how Nix’s reproducibility complements SBOM generation. The Claude Skills 360 bundle includes Nix skill sets covering flake configuration, devShell setup, and NixOS module patterns. Start with the free tier to try Nix development environment generation.