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:

  1. 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=";
         };
    
  2. 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
         '';
    
  3. 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"
     '';
    
  4. 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