Packaging Claude Code on NixOS
I’ve been heavily using claude-code since its release and have found it to be one of the most powerful LLM-assisted coding tools on the market today.
There was just one problem: it’s not available on NixOS which my home computer is running on. I decided to package it myself.
Fortunately, the packaging process turned out to be relatively straightforward, though with a few interesting challenges. Since claude-code isn’t open-source, we need to work with the npm-hosted version.
I’m documenting my packaging approach below to help fellow NixOS users who want to use this tool.
{ lib, buildNpmPackage, fetchurl, nodejs, makeWrapper, writeShellScriptBin }:
let
# Main package
claudeCode = buildNpmPackage rec {
pname = "claude-code";
version = "0.2.29";
src = fetchurl {
url = "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-${version}.tgz";
hash = "sha256-1iKDtTE+cHXMW/3zxfsNFjMGMxJlIBzGEXWtTfQfSMM=";
};
npmDepsHash = "sha256-fuJE/YTd9apAd1cooxgHQwPda5js44EmSfjuRVPbKdM=";
inherit nodejs;
makeCacheWritable = true;
postPatch = ''
if [ -f "${./claude-code/package-lock.json}" ]; then
echo "Using vendored package-lock.json"
cp "${./claude-code/package-lock.json}" ./package-lock.json
else
echo "No vendored package-lock.json found, creating a minimal one"
exit 1
fi
'';
dontNpmBuild = true;
dontNpmInstall = true;
nativeBuildInputs = [ makeWrapper ];
# Create a custom installation phase to handle the package organization
installPhase = ''
# Create a directory for the lib files
mkdir -p $out/lib/node_modules/@anthropic-ai/claude-code
# Copy all package files to the lib directory
cp -a . $out/lib/node_modules/@anthropic-ai/claude-code/
# Create bin directory
mkdir -p $out/bin
# Create a wrapper script that points to the actual CLI script
makeWrapper ${nodejs}/bin/node $out/bin/claude-code \
--add-flags "$out/lib/node_modules/@anthropic-ai/claude-code/cli.mjs"
'';
meta = with lib; {
description = "Claude Code CLI tool";
homepage = "https://www.anthropic.com/claude-code";
mainProgram = "claude-code";
};
};
# Helper script to update the package-lock.json file
#
# Build with `nix build .#claude-code.updateScript`
updateScript = writeShellScriptBin "update-claude-code-lock" ''
#!/usr/bin/env bash
set -e
if [ $# -ne 1 ]; then
echo "Usage: update-claude-code-lock <version>"
echo "Example: update-claude-code-lock 0.2.29"
exit 1
fi
VERSION="$1"
TEMP_DIR=$(mktemp -d)
LOCK_DIR="$PWD/packages/claude-code"
echo "Creating $LOCK_DIR if it doesn't exist..."
mkdir -p "$LOCK_DIR"
echo "Downloading claude-code version $VERSION..."
curl -L "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-$VERSION.tgz" -o "$TEMP_DIR/claude-code.tgz"
echo "Extracting tarball..."
mkdir -p "$TEMP_DIR/extract"
tar -xzf "$TEMP_DIR/claude-code.tgz" -C "$TEMP_DIR/extract"
echo "Generating package-lock.json..."
cd "$TEMP_DIR/extract/package"
${nodejs}/bin/npm install --package-lock-only --ignore-scripts
echo "Copying package-lock.json to $LOCK_DIR..."
cp package-lock.json "$LOCK_DIR/"
echo "Cleaning up..."
rm -rf "$TEMP_DIR"
echo "Done. Package lock file updated at $LOCK_DIR/package-lock.json"
echo "You may need to update the npmDepsHash in your claude-code.nix file."
echo "Use: prefetch-npm-deps $LOCK_DIR/package-lock.json"
'';
in
# Return both the package and the update script
claudeCode // {
updateScript = updateScript;
passthru = (claudeCode.passthru or {}) // {
inherit updateScript;
};
}
Here are the few highlights about the packaging process:
-
The package needs to be sourced directly from the npm registry:
src = fetchurl { url = "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-${version}.tgz"; hash = "sha256-1iKDtTE+cHXMW/3zxfsNFjMGMxJlIBzGEXWtTfQfSMM="; }; -
We vendor the package-json.lock file because the source does not include it.
postPatch = '' if [ -f "${./claude-code/package-lock.json}" ]; then echo "Using vendored package-lock.json" cp "${./claude-code/package-lock.json}" ./package-lock.json else echo "No vendored package-lock.json found, creating a minimal one" exit 1 fi ''; -
To make future updates easier, I’ve included a helper script that automatically generates the package-lock.json file from the npm registry:
# Helper script to update the package-lock.json file # # Build with `nix build .#claude-code.updateScript` updateScript = writeShellScriptBin "update-claude-code-lock" '' #!/usr/bin/env bash set -e if [ $# -ne 1 ]; then echo "Usage: update-claude-code-lock <version>" echo "Example: update-claude-code-lock 0.2.29" exit 1 fi VERSION="$1" TEMP_DIR=$(mktemp -d) LOCK_DIR="$PWD/packages/claude-code" echo "Creating $LOCK_DIR if it doesn't exist..." mkdir -p "$LOCK_DIR" echo "Downloading claude-code version $VERSION..." curl -L "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-$VERSION.tgz" -o "$TEMP_DIR/claude-code.tgz" echo "Extracting tarball..." mkdir -p "$TEMP_DIR/extract" tar -xzf "$TEMP_DIR/claude-code.tgz" -C "$TEMP_DIR/extract" echo "Generating package-lock.json..." cd "$TEMP_DIR/extract/package" ${nodejs}/bin/npm install --package-lock-only --ignore-scripts echo "Copying package-lock.json to $LOCK_DIR..." cp package-lock.json "$LOCK_DIR/" echo "Cleaning up..." rm -rf "$TEMP_DIR" echo "Done. Package lock file updated at $LOCK_DIR/package-lock.json" echo "You may need to update the npmDepsHash in your claude-code.nix file." echo "Use: prefetch-npm-deps $LOCK_DIR/package-lock.json" ''; -
The standard npm install phase doesn’t work for this package, so we implement a custom approach:
dontNpmBuild = true; dontNpmInstall = true; nativeBuildInputs = [ makeWrapper ]; # Create a custom installation phase to handle the package organization installPhase = '' # Create a directory for the lib files mkdir -p $out/lib/node_modules/@anthropic-ai/claude-code # Copy all package files to the lib directory cp -a . $out/lib/node_modules/@anthropic-ai/claude-code/ # Create bin directory mkdir -p $out/bin # Create a wrapper script that points to the actual CLI script makeWrapper ${nodejs}/bin/node $out/bin/claude-code \ --add-flags "$out/lib/node_modules/@anthropic-ai/claude-code/cli.mjs" '';
This wrapper script approach keeps the binary directory clean while ensuring the CLI has access to all its required dependencies.
EDIT(2025-03-03): phanirithvij rightfully pointed out that the package has been merged to nixos-unstable: claude-code: init at 0.2.9 by malob