From 52e759d5d9eca7cfcf3b5fe4a1904a194976901b Mon Sep 17 00:00:00 2001 From: Alexandre Balmes Date: Thu, 26 Feb 2026 00:54:57 +0100 Subject: [PATCH] feat(claude-code): switch to official installer with volume persistence - `README.md`: Update docs to reflect official installer and automatic config persistence - `src/claude-code/devcontainer-feature.json`: Bump to 1.1.0, add named volume mount and postStartCommand, drop Node.js dependency - `src/claude-code/install.sh`: Replace npm install with official claude.ai installer, copy binary to /usr/local/bin, install link-claude-config script - `src/claude-code/link-claude-config.sh`: Add post-start script to symlink ~/.claude to /claude-config volume - `test/claude-code/install_claude_code_specific_version.sh`: Replace npm list version check with claude --version - `test/claude-code/test.sh`: Remove npm global package validation (no longer applicable) --- README.md | 29 +++-------- src/claude-code/devcontainer-feature.json | 17 ++++--- src/claude-code/install.sh | 50 ++++++++++--------- src/claude-code/link-claude-config.sh | 32 ++++++++++++ .../install_claude_code_specific_version.sh | 4 +- test/claude-code/test.sh | 7 --- 6 files changed, 79 insertions(+), 60 deletions(-) create mode 100644 src/claude-code/link-claude-config.sh diff --git a/README.md b/README.md index 39540ff..869cc3c 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Flutter and Dart binaries are added to `PATH` automatically. ### Claude Code -Installs [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Anthropic's official CLI for Claude. Supports latest and pinned npm versions. +Installs [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Anthropic's official CLI for Claude via the official installer. Supports latest and pinned versions. ```jsonc // devcontainer.json @@ -88,7 +88,7 @@ Installs [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Anthropi | Option | Type | Default | Description | |--------|------|---------|-------------| -| `version` | string | `latest` | npm version: `latest` or a specific version (e.g. `1.0.3`) | +| `version` | string | `latest` | Version to install: `latest` or a specific version (e.g. `1.0.3`) | #### Examples @@ -118,28 +118,15 @@ The feature does not set `ANTHROPIC_API_KEY` automatically. Pass it from your ho This injects the key at runtime without baking it into the Docker image layer. -#### Persisting Configuration +#### Persistent Configuration -To persist your Claude config across container rebuilds, add bind mounts to your `devcontainer.json`: +Configuration persists automatically via a Docker named volume mounted at `/claude-config`. On each container start, a symlink `~/.claude -> /claude-config` is created for the logged-in user. -```jsonc -{ - "mounts": [ - { - "source": "${localEnv:HOME}/.claude", - "target": "/home/vscode/.claude", - "type": "bind" - }, - { - "source": "${localEnv:HOME}/.claude.json", - "target": "/home/vscode/.claude.json", - "type": "bind" - } - ] -} -``` +- No configuration required — it works out of the box +- Authentication and settings survive container rebuilds +- Each devcontainer gets its own isolated volume (`claude-code-config-`) -Requires the [Node.js feature](https://github.com/devcontainers/features/tree/main/src/node) (`ghcr.io/devcontainers/features/node`). +No additional features required — Claude Code is installed as a standalone binary. --- diff --git a/src/claude-code/devcontainer-feature.json b/src/claude-code/devcontainer-feature.json index ce2f49d..a2f99ba 100644 --- a/src/claude-code/devcontainer-feature.json +++ b/src/claude-code/devcontainer-feature.json @@ -1,15 +1,15 @@ { "id": "claude-code", - "version": "1.0.0", + "version": "1.1.0", "name": "Claude Code", - "description": "Installs Claude Code, Anthropic's official CLI for Claude. Supports latest and pinned npm versions. To pass your API key, add '\"remoteEnv\": { \"ANTHROPIC_API_KEY\": \"${localEnv:ANTHROPIC_API_KEY}\" }' in your devcontainer.json. To persist config, bind mount ~/.claude and ~/.claude.json.", + "description": "Installs Claude Code, Anthropic's official CLI for Claude. Supports latest and pinned versions. Configuration persists automatically via a Docker volume.", "documentationURL": "https://docs.anthropic.com/en/docs/claude-code", "licenseURL": "https://github.com/anthropics/claude-code/blob/main/LICENSE", "options": { "version": { "type": "string", "default": "latest", - "description": "npm version to install: 'latest' or a specific version (e.g. '1.0.3')" + "description": "Version to install: 'latest' or a specific version (e.g. '1.0.3')" } }, "customizations": { @@ -24,7 +24,12 @@ ] } }, - "installsAfter": [ - "ghcr.io/devcontainers/features/node" - ] + "mounts": [ + { + "source": "claude-code-config-${devcontainerId}", + "target": "/claude-config", + "type": "volume" + } + ], + "postStartCommand": "/usr/local/bin/link-claude-config" } diff --git a/src/claude-code/install.sh b/src/claude-code/install.sh index a453d70..5289e7c 100644 --- a/src/claude-code/install.sh +++ b/src/claude-code/install.sh @@ -6,39 +6,41 @@ VERSION="${VERSION:-latest}" echo "==> Claude Code feature: version=${VERSION}" -# Node.js and npm are required — install if missing -if ! command -v npm &>/dev/null; then - echo "==> npm not found, installing Node.js..." +# Ensure curl is available (minimal images may not have it) +if ! command -v curl &>/dev/null; then + echo "==> curl not found, installing..." apt-get update - apt-get install -y curl ca-certificates gnupg - mkdir -p /etc/apt/keyrings - curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ - | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg - echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \ - > /etc/apt/sources.list.d/nodesource.list - apt-get update - apt-get install -y nodejs + apt-get install -y curl ca-certificates fi -echo "==> npm version: $(npm --version)" -echo "==> node version: $(node --version)" - -# Build the npm package reference: latest or pinned +# Install Claude Code via official installer if [ "$VERSION" = "latest" ]; then - NPM_PACKAGE="@anthropic-ai/claude-code" + curl -fsSL https://claude.ai/install.sh | bash else - NPM_PACKAGE="@anthropic-ai/claude-code@${VERSION}" + curl -fsSL https://claude.ai/install.sh | bash -s -- "$VERSION" fi -echo "==> Installing ${NPM_PACKAGE}..." -npm install -g "$NPM_PACKAGE" - -# Verify the binary is accessible -if ! command -v claude &>/dev/null; then - echo "ERROR: 'claude' binary not found on PATH after install." - echo "npm global bin: $(npm bin -g 2>/dev/null || echo 'unknown')" +# The installer places the binary in ~/.local/bin (under the build user's $HOME). +# Copy it to /usr/local/bin so all container users can access it regardless of +# home directory permissions (e.g. /root is 700). +CLAUDE_BIN="$HOME/.local/bin/claude" +if [ ! -f "$CLAUDE_BIN" ]; then + echo "ERROR: expected binary at $CLAUDE_BIN not found after install." exit 1 fi +install -m 0755 "$CLAUDE_BIN" /usr/local/bin/claude echo "==> Claude Code $(claude --version) installed at $(command -v claude)" + +# Pre-create the volume mount point with correct ownership so that Docker +# named volumes inherit the ownership on first use (no sudo needed later). +if [ -n "${_REMOTE_USER:-}" ]; then + mkdir -p /claude-config + chown "$_REMOTE_USER:$_REMOTE_USER" /claude-config +fi + +# Install the post-start script for persistent storage symlink +install -d /usr/local/bin +install -m 0755 ./link-claude-config.sh /usr/local/bin/link-claude-config + echo "==> Claude Code feature install complete" diff --git a/src/claude-code/link-claude-config.sh b/src/claude-code/link-claude-config.sh new file mode 100644 index 0000000..484ffc0 --- /dev/null +++ b/src/claude-code/link-claude-config.sh @@ -0,0 +1,32 @@ +#!/bin/sh +set -eu + +# Auto-detect the current user +username="$(whoami 2>/dev/null || true)" + +# No username detected? Exit gracefully (volume still mounted at /claude-config) +[ -n "$username" ] || exit 0 + +# Find home directory for that user +home_dir="$(getent passwd "$username" | cut -d: -f6 || true)" +if [ -z "$home_dir" ]; then + echo "claude-config: user '$username' not found; skipping" >&2 + exit 0 +fi + +mkdir -p "$home_dir" + +# Create/replace symlink: ~/.claude -> /claude-config +echo "claude-config: $home_dir/.claude -> /claude-config" >&2 +ln -snf /claude-config "$home_dir/.claude" + +# Persist ~/.claude.json inside the volume and symlink it +config_file="/claude-config/claude.json" +target_file="$home_dir/.claude.json" +if [ -f "$target_file" ] && [ ! -L "$target_file" ]; then + # First run: move existing file into the volume + mv "$target_file" "$config_file" +fi +touch "$config_file" +echo "claude-config: $target_file -> $config_file" >&2 +ln -snf "$config_file" "$target_file" diff --git a/test/claude-code/install_claude_code_specific_version.sh b/test/claude-code/install_claude_code_specific_version.sh index 1924300..027d627 100644 --- a/test/claude-code/install_claude_code_specific_version.sh +++ b/test/claude-code/install_claude_code_specific_version.sh @@ -7,10 +7,10 @@ set -e source "$(dirname "$0")/test.sh" # Additional check: verify the exact version was installed -INSTALLED=$(npm list -g @anthropic-ai/claude-code --depth=0 2>/dev/null | grep @anthropic-ai/claude-code | grep -oP '\d+\.\d+\.\d+') +INSTALLED=$(claude --version 2>&1 | grep -oP '\d+\.\d+\.\d+') EXPECTED="2.1.30" if [ "$INSTALLED" != "$EXPECTED" ]; then - echo "FAIL: Expected @anthropic-ai/claude-code@${EXPECTED}, got ${INSTALLED}" + echo "FAIL: Expected version ${EXPECTED}, got ${INSTALLED}" exit 1 fi echo "PASS: Correct version ${EXPECTED} installed" diff --git a/test/claude-code/test.sh b/test/claude-code/test.sh index 1e9d038..7cc7a8d 100644 --- a/test/claude-code/test.sh +++ b/test/claude-code/test.sh @@ -21,11 +21,4 @@ if [ -z "$CLAUDE_VERSION" ]; then fi echo "PASS: claude --version => ${CLAUDE_VERSION}" -# npm package must be registered globally -if ! npm list -g @anthropic-ai/claude-code --depth=0 &>/dev/null; then - echo "FAIL: @anthropic-ai/claude-code not found in npm global packages" - exit 1 -fi -echo "PASS: @anthropic-ai/claude-code present in npm global packages" - echo "==> All Claude Code feature tests passed"