Nix flake (flakes + nix-command) Assumed development environment setup reference. Templates for each language are made assets/ summarized into,cp usable with a single command.
When to use
- "Create a nix dev shell" / "Write a flake.nix"
- "I want to add nix to a new project"
- "Make nix usable in claude code web / containers"
flake.nix/flake.lock/.envrcgeneral questions aboutnix develop/nix buildtroubleshooting
What's in assets/
assets/
├── setup_nix.sh # 単一ユーザー Nix を sandbox 無効で入れるインストーラ (container / CCW 向け)
├── apm.nix # microsoft/apm (Agent Package Manager) の Nix derivation
├── moonbit/{flake.nix,.envrc} # moonbit-overlay + moon
├── rust/{flake.nix,.envrc} # rust-overlay + stable pinned + cargo-nextest/watch
├── typescript/{flake.nix,.envrc} # nodejs_24 + pnpm (top-level)
├── python-uv/{flake.nix,.envrc} # python3 + uv
├── haskell/{flake.nix,.envrc} # GHC + cabal + HLS + hlint + ormolu
├── ocaml/{flake.nix,.envrc,.gitignore} # OCaml 5 + dune + merlin + ocaml-lsp + utop
├── oxcaml/{flake.nix,.envrc,.gitignore} # Jane Street OxCaml (opam source build)
└── home-manager/ # multi-host home-manager flake (macos + ccweb)
├── flake.nix
├── common.nix
├── macos.nix # aarch64-darwin: full desktop
├── ccweb.nix # x86_64-linux: minimal sandbox
└── .gitignore # private.nix / *.secret.nix を除外
All templates include just, ast-grep, apm (common operational tool).
The template is "at the project root flake.nix + apm.nix are placed side by side." If you want to swap out the language side, apm.nix can be reused.
apm.nix is optional: Can be deleted if the project does not use the APM skill. In that case, flake.nix from apm = import ./apm.nix ... と packages inside apm delete both.just / ast-grep If you only want to keep that, leave it as is.
Quick install
macOS / Linux (recommended)
Determinate Systems installer use.experimental-features = nix-command flakes is enabled from the start, and uninstallation is easy.
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
Claude Code web / other sandbox environments
For environments where systemd cannot be used, only root exists, or nested namespaces are prohibited, use assets/setup_nix.sh .
build-users-group =(empty) for single-user modesandbox = falsedoes not require chrootexperimental-features = nix-command flakesを/etc/nix/nix.confと$HOME/.config/nix/nix.confwrite in both/etc/profile.d/nix.shplace it so PATH is available in subsequent shells as well
cp ~/.claude/skills/nix-setup/assets/setup_nix.sh .
bash setup_nix.sh
. "$HOME/.nix-profile/etc/profile.d/nix.sh" # 現在の shell にも反映
nix --version
Minimal Flake skeleton
{
description = "...";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = import nixpkgs { inherit system; };
in {
devShells.default = pkgs.mkShell {
packages = [ /* ... */ ];
shellHook = ''...'';
};
});
}
nix develop で devShells.default goes into.direnv if .envrc に use flake write and direnv allow。
Common tools (included in all templates)
| tool | Purpose | Why include it every time |
|---|---|---|
just |
task runner | justfile is the project standard |
ast-grep |
Structural search / lint | Statically enforce rules that cannot be written with grep |
apm |
Agent dependency manager | apm.yml Distribute skills/prompts in a reproducible form with |
How to use language-specific templates
To introduce a template, flake.nix + apm.nix place side by side in the project root just do:
# 例: MoonBit プロジェクト
cp ~/.claude/skills/nix-setup/assets/apm.nix .
cp ~/.claude/skills/nix-setup/assets/moonbit/flake.nix .
nix develop # 初回はビルド
# direnv 派なら:
echo 'use flake' > .envrc && direnv allow
MoonBit
moonbit-overlay.packages.${system}.moon-patched_latestuse- The overlay is pinned to known working versions (
50118f5c3c0298b5cb17cc6f1c346165801014c8). The latest HEAD may depend on packages treated as broken in nixpkgs-unstable (e.g., tcc), and there are periods when even evaluation does not pass. After confirming it works, update moon.mod.jsonif there ismoon updateautomatically run (first time only)- Reference: vibe-lang's
flake.nix
Rust
rust-overlaymanage toolchain with- default is
stable.latest.default+ rust-analyzer / clippy / rustfmt - Pin to a specific version:
rust-bin.stable."1.91.0".default rust-toolchain.tomlintegration:rust-bin.fromRustupToolchainFile ./rust-toolchain.toml- WASM target:
targets = [ "wasm32-wasip1" "wasm32-unknown-unknown" ]
TypeScript
nodejs_24+pkgs.pnpm(top-level)nodePackages.*was removed from nixpkgs in 2025 —nodePackages.pnpmcannot be usedPNPM_HOMEproject-local ($PWD/.pnpm) toward$HOMEavoid contamination- If switching to npm / yarn,
pkgs.pnpmremove andpkgs.nodejs_24use thenpmincluded with, orpkgs.yarnadd
Python + uv
pkgs.python3+pkgs.uv- Since uv manages the Python version itself, the nix side is treated as a fallback
UV_PROJECT_ENVIRONMENT=$PWD/.venv,UV_CACHE_DIR=$PWD/.uv-cachedoes not pollute $HOME
Haskell
pkgs.haskellPackages.{ghc,cabal-install,haskell-language-server,hlint,ormolu,ghcid}- nixpkgs standard (
haskellPackages) configuration using. GHC version pinning ispkgs.haskell.packages.ghc98replace with etc. - If even stricter reproducibility is needed,
haskell.nixConsider (IOHK). However, the learning cost and complexity jump up. - If using stack, add
pkgs.stack, and switch to a policy of pinning GHC withstack.yamlのresolver. - on macOS
stdenv.cc.cc.libis not in PATH, so some FFI runs into trouble → appendbuildInputs = [ pkgs.zlib ]etc.
OCaml
pkgs.ocamlPackages.{ocaml,dune_3,findlib,merlin,ocaml-lsp,ocamlformat,utop}+pkgs.opam- nixpkgs standard OCaml 5.x series. Pinned versions include
pkgs.ocaml-ng.ocamlPackages_5_2etc. - opam managed: shellHook が
OPAMROOT=$PWD/.opamconfines state to the project local, and only the first time automatically runsopam init+opam switch create default --empty. From the second time onward,opam envonlyevaldo - By using an empty switch, the nix-provided ocaml is used as-is, avoiding double management of the compiler.
.gitignoreに.opam/is included, so do not accidentally commit state.- Additional libraries are placed project-locally with
opam install <pkg>. merlinはocaml-lspdependencies. Having both installed makes editor integration complete.
OxCaml (Jane Street's fork)
- Not included in nixpkgs. Following the officially recommended "opam + source build" flow, the nix side provides only
opamと build toolchain (autoconf,automake,m4,pkg-config,gmp,libffi). - On the first execution of shellHook:
$OPAMROOT/.opamcreateoxcaml-devcreate switch as emptyopam pin add -ny git+https://github.com/oxcaml/oxcamlで upstream repo を pinopam switch set-invariant --packages oxcaml-devset invariant with
- Do not automatically run the actual compiler build(takes 5-20 minutes / ~1 GB). The user explicitly runs the following:
opam install oxcaml-dev opam install dune merlin ocaml-lsp-server ocamlformat utop - Support: arm64 macOS / arm64 Linux / x86_64 Linux. x86_64 macOS works but is unofficially supported.
- With the ocaml template, it "uses the same opam infrastructure but a different switch", so both templates can be switched and used separately.
apm.nix mechanism
microsoft/apm is not an npm package but a PyInstaller-bundled native binaryIt is distributed via
_internal/includes the Python 3.12 runtime + dependency libraries- Standalone
apmbinary is_internalis referenced in the co-located directory, so it cannot be extracted and used. - on Linux
autoPatchelfHookRewrite links to glibc / libstdc++ / zlib to those in the nix store $out/libexec/apm/Place it entirely in$out/bin/apmis amakeWrapperwrapper to itdontStrip = true+dontPatchELF = trueis required — PyInstaller appends a PKG archive to the end of Mach-O / ELF. stdenv's default strip / patchelf trims the end, so unless this is disabled, it fails at runtime withCould not load PyInstaller's embedded PKG archive.
Version update procedure:
# 1. 新しいリリースの SHA256 を取得
for a in darwin-arm64 darwin-x86_64 linux-arm64 linux-x86_64; do
curl -sSL "https://github.com/microsoft/apm/releases/download/vX.Y.Z/apm-${a}.tar.gz.sha256"
done
# 2. assets/apm.nix の version と sources.*.sha256 を差し替え
Handling npm tools with Nix (as of 2026)
nodePackages.* The correct path after the abolition of
pkgs.pnpm/ top-level packages — This first. If it is in nixpkgs, use it.buildNpmPackage+importNpmLock— Build official packages with lockfiles yourself. Fully pinned.NPM_CONFIG_PREFIX="$PWD/.npm-global"を shellHook で — Impure but project-scoped. An escape hatch when hash tracking is heavy.npx/pnpm dlx— A CLI used only occasionally.
Avoid: npm install -g WritingnodePackages.* refer todream2nix / node2nix newly adopt
buildNpmPackage skeleton
my-cli = pkgs.buildNpmPackage {
pname = "my-cli";
version = "1.2.3";
src = pkgs.fetchFromGitHub {
owner = "owner"; repo = "my-cli"; rev = "v1.2.3";
hash = "sha256-...";
};
# lockfile ベースに切り替えるなら:
# npmDeps = pkgs.importNpmLock { npmRoot = ./.; };
npmDepsHash = "sha256-...";
dontNpmBuild = true; # pure CLI なら build なし
};
home-manager (optional)
If you want to declaratively manage your entire user environment (zsh / git / starship / CLI packages) with Nix expressions, use assets/home-manager/ . When using it together with chezmoi, dot_zshrc etc. to chezmoi forget and then transfer ownership to home-manager.
File structure
flake.nix— two outputs: macOS desktop profile (macos) and for Claude Code web / ephemeral Linux (ccweb)common.nix— common to all hosts (zsh, git, starship, direnv, fzf, basic CLI)macos.nix— aarch64-darwin specific (heavy tools like helix, neovim)ccweb.nix— x86_64-linux minimal configuration (keep it thin with a target cold start < 2 minutes)
Usage
# 1. コピー
cp -r ~/.claude/skills/nix-setup/assets/home-manager ~/.config/home-manager
cd ~/.config/home-manager
# 2. flake.nix 先頭の `username` / `email` を書き換え
$EDITOR flake.nix
# 3. 適用
home-manager switch --flake .#macos # macOS
home-manager switch --flake .#ccweb # Claude Code web 等 Linux
Notes when managing in a public repo
This template is written with zero secrets (username / email is a flake.nix variable, and does not include hostnames, tokens, or known_hosts). It is OK to put it on public GitHub.
For your own private secrets (SSH config, internal hosts, API tokens), use one of the following:
./private.nixcreateimportsand load it with.gitignoreexclude with (included in the template)sops-nix/agenixcommit while encrypted with
Cold start guideline on Claude Code web
| Step | with cache.nixos.org | bare |
|---|---|---|
setup_nix.sh |
20-40 seconds | same as left |
| flake evaluation + substitute | 30-90 seconds | 2-8 minutes |
| activation (symlink expansion) | a few seconds | same as left |
| Total | 1-2 minutes | 4-12 minutes |
ccweb.nix The more heavy packages you add to , the longer the cold start becomes. Keep the common parts in common.nix and follow the policy of writing only additional packages inccweb.nix .
Introducing into an existing repo
Unlike a new project (cp assets/<lang>/flake.nix .), an existing repo has the following pitfalls.
1. cp back up existing files before
The template's cp overwrites the existing .envrc / flake.nix / .gitignore as is. Follow the backup → merge procedure:
# 既存を保存
[ -f .envrc ] && cp .envrc .envrc.pre-nix
# テンプレを展開
cp ~/.claude/skills/nix-setup/assets/typescript/flake.nix .
cp ~/.claude/skills/nix-setup/assets/typescript/.envrc .
# 既存の export などを復元
# .envrc.pre-nix を参照しつつ手でマージ
2. .envrc merge policy for
If there is an existing .envrc に export DATABASE_URL=... etc.,use flake を beginning placed in the existing export is left behind (based on the devShell env, overridden with repo-specific values).
# 正しい順序
use flake # まず devShell を起動
dotenv_if_exists .env.local # secrets
export DATABASE_URL="postgres://..." # 既存 export を温存
Conversely, export is written first, use flake may be overridden by
3. lockfile migration (npm → pnpm, etc.)
package-lock.json と pnpm-lock.yaml mixing is a source of undefined behavior. If migrating, make it a separate PR and separate it from the Nix conversion:
git switch -c chore/pnpm-migration
rm package-lock.json && rm -rf node_modules
corepack enable && corepack prepare pnpm@10 --activate
pnpm install # 再解決
pnpm why <critical-dep> # major drift 確認
pnpm build && pnpm test # 動作確認
git add pnpm-lock.yaml package.json && git rm package-lock.json
git commit -m "chore: migrate npm -> pnpm lockfile"
During rebase, if pnpm-lock.yaml conflicts, do not fix it by hand pnpm install regenerate with git add。
4. CI の actions/setup-node replace
Rewrite the Node-related steps in the existing ci.yml with the Nix conversion diff:
- - uses: actions/setup-node@v4
- with:
- node-version: 24
- cache: pnpm
- - run: pnpm install --frozen-lockfile
- - run: pnpm test
+ - uses: DeterminateSystems/nix-installer-action@main
+ - uses: DeterminateSystems/magic-nix-cache-action@main
+ - run: nix develop --command just ci
just ci on the pnpm install --frozen-lockfile && pnpm build && pnpm test side, define actions/cache (move it to the justfile). Keeping the pnpm store separately with
5. Handling monorepos
pnpm-workspace.yaml For monorepos using / turborepo / Nx, place one flake.nix at the root and it will be shared across all workspaces nix develop is shared. Keep dev tools specific to each package in package.json 's devDependencies, and limit nix to language runtimes + cross-cutting tools (just / ast-grep).
6. Existing .gitignore integration with
assets/ocaml/.gitignore や assets/oxcaml/.gitignore Copying .gitignore overwrites the existing
result/result-*(Nix build output) must be added.direnv/(nix-direnv cache) is added
direnv integration
Each language template includes .envrc is already bundled.flake.nix If you copy it together with cd the devShell is automatically applied at
cp ~/.claude/skills/nix-setup/assets/apm.nix .
cp ~/.claude/skills/nix-setup/assets/rust/flake.nix .
cp ~/.claude/skills/nix-setup/assets/rust/.envrc .
direnv allow # 最初だけ承認
nix-direnv is required — with plain direnv, flake evaluation runs every time you cd and you wait 10-60 seconds.nix-direnv creates a cache + GC root and reduces it to < 100ms.assets/home-manager/common.nix In programs.direnv.nix-direnv.enable = true it is already enabled with
Bundled .envrc patterns:
- moonbit:
use flakeonly (minimal) - rust:
use flake+watch_file rust-toolchain.toml— automatic reload when switching toolchains - typescript / python-uv:
use flake+dotenv_if_exists .env.local— loading local API keys, etc.
Editor integration
VS Code の direnv.direnv If you install the direnv plugin for the extension / Helix / Neovim, the LSP server inherits the devShell PATH. rust-analyzer uses flake.nix 's rustToolchain,moon ide picks up the overlay version of moon, and so on, achieving consistency.
Cleaning up GC roots
nix-direnv-prune # 使われていない .direnv を検出して除去
About once every six months./nix/store When
GitHub Actions
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Run tests in dev shell
run: nix develop --command just test
magic-nix-cache-action has grown too large.
puts the Nix store on GitHub Actions' free cache. For personal projects it is sufficient. For large-scale use, consider Cachix / attic.
error: experimental Nix feature 'nix-command' is disabled
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
error: cannot build on ... due to sandbox
Troubleshooting/etc/nix/nix.conf に sandbox = false In container / rootless environments, the sandbox does not work. Writesetup_nix.sh does this).
error: attribute 'pnpm' missing / nodePackages.pnpm cannot be found
nixpkgs >= 24.11 In nodePackages.* has been removed.pkgs.pnpm Use (top-level).
macOS で /nix/store permission error
The Determinate installer is recommended. If doing it manually, APFS volume splitting is required on Apple Silicon.
autoPatchelfHook failed (libstdc++.so.6 not found)
buildInputs に stdenv.cc.cc.lib added.apm.nix The same workaround as done in .
Updating flake inputs
nix flake update # 全 input
nix flake update nixpkgs # 個別