From 324a5a3d79a71e051ea534280ee5c9d86fbe2d3d Mon Sep 17 00:00:00 2001 From: AnExiledDev Date: Wed, 25 Feb 2026 00:38:30 +0000 Subject: [PATCH 1/3] Replace npm Claude Code with native binary installation The Anthropic devcontainer feature installed Claude Code via npm as root, making the package directory unwritable by the vscode user. This caused the in-session auto-updater to fail with EACCES on every update attempt. Replaces ghcr.io/anthropics/devcontainer-features/claude-code (npm) with a new ./features/claude-code-native feature that uses Anthropic's official native installer (https://claude.ai/install.sh). The native binary installs to ~/.local/bin/claude owned by the container user, so claude update works without permission issues. --- .devcontainer/CHANGELOG.md | 7 ++ .devcontainer/CLAUDE.md | 2 +- .devcontainer/devcontainer.json | 6 +- .../features/claude-code-native/README.md | 47 ++++++++++ .../devcontainer-feature.json | 29 +++++++ .../features/claude-code-native/install.sh | 87 +++++++++++++++++++ .devcontainer/scripts/setup-aliases.sh | 10 +-- .devcontainer/scripts/setup-update-claude.sh | 45 ++++------ .devcontainer/scripts/setup.sh | 2 +- 9 files changed, 195 insertions(+), 40 deletions(-) create mode 100644 .devcontainer/features/claude-code-native/README.md create mode 100644 .devcontainer/features/claude-code-native/devcontainer-feature.json create mode 100755 .devcontainer/features/claude-code-native/install.sh diff --git a/.devcontainer/CHANGELOG.md b/.devcontainer/CHANGELOG.md index 29581b4..9dbe7cd 100644 --- a/.devcontainer/CHANGELOG.md +++ b/.devcontainer/CHANGELOG.md @@ -12,6 +12,13 @@ ### Changed +#### Claude Code Installation +- **Replaced npm installation with native binary** — swapped `ghcr.io/anthropics/devcontainer-features/claude-code:1.0.5` (npm-based) for new `./features/claude-code-native` feature that installs via Anthropic's official native installer (`https://claude.ai/install.sh`) +- **In-session auto-updater now works** — native binary installs to `~/.local/bin/claude` owned by the container user, so `claude update` can write freely without root permission issues +- **setup-update-claude.sh** — stripped all npm fallback and `claude install` bootstrap code; now native-binary-only with 60s timeout and transitional npm cleanup +- **setup-aliases.sh** — simplified `_CLAUDE_BIN` resolution to native binary path only (removed npm and `/usr/local/bin` fallbacks) +- **setup.sh** — fixed background update script invocation to capture all output to log file instead of discarding via `&>/dev/null` + #### System Prompt - **`` section** — Updated to document Claude Code native worktree convention (`/.claude/worktrees/`) as the recommended approach alongside the legacy `.worktrees/` convention. Added `EnterWorktree` tool guidance, `.worktreeinclude` file documentation, and path convention comparison table. diff --git a/.devcontainer/CLAUDE.md b/.devcontainer/CLAUDE.md index 62295b1..c81e924 100644 --- a/.devcontainer/CLAUDE.md +++ b/.devcontainer/CLAUDE.md @@ -173,4 +173,4 @@ Labels are `custom-text` widgets with `merge: "no-padding"` so they fuse visuall ## Features -Custom features in `./features/` follow the [devcontainer feature spec](https://containers.dev/implementors/features/). Every local feature supports `"version": "none"` to skip installation. Claude Code is installed via `ghcr.io/anthropics/devcontainer-features/claude-code:1`. +Custom features in `./features/` follow the [devcontainer feature spec](https://containers.dev/implementors/features/). Every local feature supports `"version": "none"` to skip installation. Claude Code is installed as a native binary via `./features/claude-code-native` (uses Anthropic's official installer at `https://claude.ai/install.sh`). diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ae37aff..13f780d 100755 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -45,7 +45,7 @@ }, // Feature install order: external runtimes first (Node, uv, Rust, Bun), - // then Claude Code (needs Node), then custom features. + // then Claude Code native binary (no Node dependency), then custom features. // npm-dependent features (agent-browser, ccusage, ccburn, claude-session-dashboard, // biome, lsp-servers) must come after Node. uv-dependent features (ruff, claude-monitor) must // come after uv. cargo-dependent features (ccms) must come after Rust. @@ -57,7 +57,7 @@ "ghcr.io/devcontainers-extra/features/uv", "ghcr.io/rails/devcontainer/features/bun", "ghcr.io/devcontainers/features/rust", - "ghcr.io/anthropics/devcontainer-features/claude-code", + "./features/claude-code-native", "./features/tmux", "./features/agent-browser", "./features/claude-monitor", @@ -96,7 +96,7 @@ }, // Uncomment to add Go runtime (not installed by default): // "ghcr.io/devcontainers/features/go:1": {}, - "ghcr.io/anthropics/devcontainer-features/claude-code:1.0.5": {}, + "./features/claude-code-native": {}, "./features/tmux": {}, "./features/ccusage": { "version": "latest", diff --git a/.devcontainer/features/claude-code-native/README.md b/.devcontainer/features/claude-code-native/README.md new file mode 100644 index 0000000..72e3fd2 --- /dev/null +++ b/.devcontainer/features/claude-code-native/README.md @@ -0,0 +1,47 @@ +# Claude Code CLI (Native Binary) + +Installs [Claude Code](https://docs.anthropic.com/en/docs/claude-code) as a native binary using Anthropic's official installer. + +Unlike the npm-based installation (`ghcr.io/anthropics/devcontainer-features/claude-code`), this feature installs the native binary directly to `~/.local/bin/claude`. The binary is owned by the container user, so the in-session auto-updater works without permission issues. + +## Options + +| Option | Default | Description | +|--------|---------|-------------| +| `version` | `latest` | `latest`, `stable`, or a specific semver (e.g., `2.1.52`). Set to `none` to skip. | +| `username` | `automatic` | Container user to install for. `automatic` detects from `$_REMOTE_USER`. | + +## How it works + +1. Downloads the official installer from `https://claude.ai/install.sh` +2. Runs it as the target user (not root) +3. The installer handles platform detection, checksum verification, and binary placement +4. Binary is installed to `~/.local/bin/claude` with versions stored in `~/.local/share/claude/versions/` + +## Usage + +```json +{ + "features": { + "./features/claude-code-native": {} + } +} +``` + +With version pinning: + +```json +{ + "features": { + "./features/claude-code-native": { + "version": "2.1.52" + } + } +} +``` + +## Why native over npm? + +The npm installation (`npm install -g @anthropic-ai/claude-code`) runs as root during the Docker build, creating a package owned by `root`. When the container user tries to auto-update Claude Code in-session, it fails with `EACCES` because it can't write to the root-owned package directory. + +The native binary installs to `~/.local/` under the container user's ownership, so `claude update` works without elevated permissions. diff --git a/.devcontainer/features/claude-code-native/devcontainer-feature.json b/.devcontainer/features/claude-code-native/devcontainer-feature.json new file mode 100644 index 0000000..4df87a8 --- /dev/null +++ b/.devcontainer/features/claude-code-native/devcontainer-feature.json @@ -0,0 +1,29 @@ +{ + "id": "claude-code-native", + "version": "1.0.0", + "name": "Claude Code CLI (Native Binary)", + "description": "Installs Claude Code CLI as a native binary via the official Anthropic installer", + "documentationURL": "https://docs.anthropic.com/en/docs/claude-code", + "options": { + "version": { + "type": "string", + "description": "Version to install: 'latest', 'stable', or a specific semver. Use 'none' to skip.", + "default": "latest" + }, + "username": { + "type": "string", + "description": "Container user to install for", + "default": "automatic" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "anthropic.claude-code" + ] + } + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/common-utils:2" + ] +} diff --git a/.devcontainer/features/claude-code-native/install.sh b/.devcontainer/features/claude-code-native/install.sh new file mode 100755 index 0000000..04e091c --- /dev/null +++ b/.devcontainer/features/claude-code-native/install.sh @@ -0,0 +1,87 @@ +#!/bin/bash +set -euo pipefail + +VERSION="${VERSION:-latest}" +USERNAME="${USERNAME:-automatic}" + +# Skip installation if version is "none" +if [ "${VERSION}" = "none" ]; then + echo "[claude-code-native] Skipping installation (version=none)" + exit 0 +fi + +echo "[claude-code-native] Starting installation..." +echo "[claude-code-native] Version: ${VERSION}" + +# === VALIDATE DEPENDENCIES === +if ! command -v curl &>/dev/null && ! command -v wget &>/dev/null; then + echo "[claude-code-native] ERROR: curl or wget is required" + echo " Ensure common-utils feature is installed first" + exit 1 +fi + +# === DETECT USER === +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + if [ -n "${_REMOTE_USER:-}" ]; then + USERNAME="${_REMOTE_USER}" + elif getent passwd vscode >/dev/null 2>&1; then + USERNAME="vscode" + elif getent passwd node >/dev/null 2>&1; then + USERNAME="node" + elif getent passwd codespace >/dev/null 2>&1; then + USERNAME="codespace" + else + USERNAME="root" + fi +fi + +USER_HOME=$(getent passwd "${USERNAME}" | cut -d: -f6) +if [ -z "${USER_HOME}" ]; then + echo "[claude-code-native] ERROR: Could not determine home directory for ${USERNAME}" + exit 1 +fi + +echo "[claude-code-native] Installing for user: ${USERNAME} (home: ${USER_HOME})" + +# === PREPARE DIRECTORIES === +mkdir -p "${USER_HOME}/.local/bin" +mkdir -p "${USER_HOME}/.local/share/claude" +chown -R "${USERNAME}:" "${USER_HOME}/.local/bin" "${USER_HOME}/.local/share/claude" + +# === DETERMINE TARGET === +# The official installer accepts: stable, latest, or a specific semver +TARGET="" +if [ "${VERSION}" != "latest" ] && [ "${VERSION}" != "stable" ]; then + TARGET="${VERSION}" +else + TARGET="${VERSION}" +fi + +# === INSTALL === +# The official Anthropic installer handles: +# - Platform detection (linux/darwin, x64/arm64, glibc/musl) +# - Manifest download and checksum verification +# - Binary download to ~/.local/bin/claude (symlink to ~/.local/share/claude/versions/*) +echo "[claude-code-native] Downloading official installer..." + +if [ "${USERNAME}" = "root" ]; then + curl -fsSL https://claude.ai/install.sh | sh -s -- "${TARGET}" +else + su - "${USERNAME}" -c "curl -fsSL https://claude.ai/install.sh | sh -s -- ${TARGET}" +fi + +# === VERIFICATION === +CLAUDE_BIN="${USER_HOME}/.local/bin/claude" + +if [ -x "${CLAUDE_BIN}" ]; then + INSTALLED_VERSION=$(su - "${USERNAME}" -c "${CLAUDE_BIN} --version 2>/dev/null" || echo "unknown") + echo "[claude-code-native] ✓ Claude Code installed: ${INSTALLED_VERSION}" + echo "[claude-code-native] Binary: ${CLAUDE_BIN}" +else + echo "[claude-code-native] ERROR: Installation failed — ${CLAUDE_BIN} not found or not executable" + echo "[claude-code-native] Expected binary at: ${CLAUDE_BIN}" + ls -la "${USER_HOME}/.local/bin/" 2>/dev/null || true + exit 1 +fi + +echo "[claude-code-native] Installation complete" diff --git a/.devcontainer/scripts/setup-aliases.sh b/.devcontainer/scripts/setup-aliases.sh index 532619a..80391b8 100755 --- a/.devcontainer/scripts/setup-aliases.sh +++ b/.devcontainer/scripts/setup-aliases.sh @@ -80,14 +80,8 @@ if [ "\$TERM" = "xterm" ] || [ -z "\$TERM" ]; then fi export COLORTERM="\${COLORTERM:-truecolor}" -# Prefer native binary over npm-installed version -if [ -x "\$HOME/.local/bin/claude" ]; then - _CLAUDE_BIN="\$HOME/.local/bin/claude" -elif [ -x /usr/local/bin/claude ]; then - _CLAUDE_BIN=/usr/local/bin/claude -else - _CLAUDE_BIN=claude -fi +# Native binary (installed by claude-code-native feature) +_CLAUDE_BIN="\$HOME/.local/bin/claude" # ChromaTerm wrapper (if ct is installed, wrap claude through it) if command -v ct >/dev/null 2>&1; then diff --git a/.devcontainer/scripts/setup-update-claude.sh b/.devcontainer/scripts/setup-update-claude.sh index 8ff934c..386977c 100755 --- a/.devcontainer/scripts/setup-update-claude.sh +++ b/.devcontainer/scripts/setup-update-claude.sh @@ -25,46 +25,37 @@ fi # === CLEANUP TRAP === cleanup() { - rm -f "${_TMPDIR}/claude-update" 2>/dev/null || true - rm -f "${_TMPDIR}/claude-update-manifest.json" 2>/dev/null || true rm -rf "$LOCK_FILE" 2>/dev/null || true } trap cleanup EXIT -# === VERIFY CLAUDE IS INSTALLED === -if ! command -v claude &>/dev/null; then - log "Claude Code not found, skipping update" - exit 0 -fi +# === NATIVE BINARY === +NATIVE_BIN="$HOME/.local/bin/claude" -# === ENSURE NATIVE BINARY EXISTS === -# 'claude install' puts the binary at ~/.local/bin/claude (symlink to ~/.local/share/claude/versions/*) -# Legacy manual installs used /usr/local/bin/claude — check both, prefer ~/.local -if [ -x "$HOME/.local/bin/claude" ]; then - NATIVE_BIN="$HOME/.local/bin/claude" -elif [ -x "/usr/local/bin/claude" ]; then - NATIVE_BIN="/usr/local/bin/claude" -else - NATIVE_BIN="" +if [ ! -x "$NATIVE_BIN" ]; then + log "ERROR: Native binary not found at ${NATIVE_BIN}" + log " The claude-code-native feature should install this during container build." + log " Try rebuilding the container or running: curl -fsSL https://claude.ai/install.sh | sh" + exit 1 fi -if [ -z "$NATIVE_BIN" ]; then - log "Native binary not found, installing..." - if claude install 2>&1 | tee -a "$LOG_FILE"; then - log "Native binary installed successfully" + +# === TRANSITIONAL: Remove leftover npm installation === +NPM_CLAUDE="$(npm config get prefix 2>/dev/null)/lib/node_modules/@anthropic-ai/claude-code" +if [ -d "$NPM_CLAUDE" ]; then + log "Removing leftover npm installation at ${NPM_CLAUDE}..." + if sudo npm uninstall -g @anthropic-ai/claude-code 2>/dev/null; then + log "Removed leftover npm installation" else - log "WARNING: 'claude install' failed, skipping" - exit 0 + log "WARNING: Could not remove npm installation (non-blocking)" fi - # Skip update check on first install — next start will handle it - exit 0 fi # === CHECK FOR UPDATES === CURRENT_VERSION=$("$NATIVE_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown") log "Current version: ${CURRENT_VERSION}" -# Use the official update command (handles download, verification, and versioned install) -if "$NATIVE_BIN" update 2>&1 | tee -a "$LOG_FILE"; then +# Use the official update command with timeout (handles download, verification, and versioned install) +if timeout 60 "$NATIVE_BIN" update 2>&1 | tee -a "$LOG_FILE"; then UPDATED_VERSION=$("$NATIVE_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown") if [ "$CURRENT_VERSION" != "$UPDATED_VERSION" ]; then log "Updated Claude Code: ${CURRENT_VERSION} → ${UPDATED_VERSION}" @@ -72,5 +63,5 @@ if "$NATIVE_BIN" update 2>&1 | tee -a "$LOG_FILE"; then log "Already up to date (${CURRENT_VERSION})" fi else - log "WARNING: 'claude update' failed, skipping" + log "WARNING: 'claude update' failed or timed out" fi diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index d865c67..4566e68 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -118,7 +118,7 @@ run_script "$SCRIPT_DIR/setup-terminal.sh" "$SETUP_TERMINAL" # Background the update to avoid blocking container start if [ "$SETUP_UPDATE_CLAUDE" = "true" ] && [ -f "$SCRIPT_DIR/setup-update-claude.sh" ]; then - bash "$SCRIPT_DIR/setup-update-claude.sh" &>/dev/null & + bash "$SCRIPT_DIR/setup-update-claude.sh" >>"${TMPDIR:-/tmp}/claude-update.log" 2>&1 & disown SETUP_RESULTS+=("setup-update-claude:background") else From aebb2a3f5ff512fcdff6db1308f397f84905c164 Mon Sep 17 00:00:00 2001 From: AnExiledDev Date: Wed, 25 Feb 2026 05:39:01 +0000 Subject: [PATCH 2/3] Fix CodeRabbit review issues and update stale documentation - CHANGELOG: split entries into Changed (user outcomes) and Fixed (script fixes) - install.sh: require curl explicitly, add semver validation, quote TARGET in su command, remove dead if/else - setup-update-claude.sh: use PIPESTATUS[0] to capture timeout exit code instead of tee - setup.sh: pin update log to /workspaces/.tmp/claude-update.log with override support - notify-hook, mcp-qdrant: update installsAfter from old npm feature to ./features/claude-code-native - check-setup.sh: remove stale /usr/local/bin/claude fallback --- .devcontainer/CHANGELOG.md | 14 +++++++++----- .../features/claude-code-native/install.sh | 19 +++++++++++-------- .../mcp-qdrant/devcontainer-feature.json | 2 +- .../notify-hook/devcontainer-feature.json | 2 +- .devcontainer/scripts/check-setup.sh | 2 +- .devcontainer/scripts/setup-update-claude.sh | 6 ++++-- .devcontainer/scripts/setup.sh | 4 +++- 7 files changed, 30 insertions(+), 19 deletions(-) diff --git a/.devcontainer/CHANGELOG.md b/.devcontainer/CHANGELOG.md index 9dbe7cd..067c186 100644 --- a/.devcontainer/CHANGELOG.md +++ b/.devcontainer/CHANGELOG.md @@ -13,11 +13,15 @@ ### Changed #### Claude Code Installation -- **Replaced npm installation with native binary** — swapped `ghcr.io/anthropics/devcontainer-features/claude-code:1.0.5` (npm-based) for new `./features/claude-code-native` feature that installs via Anthropic's official native installer (`https://claude.ai/install.sh`) -- **In-session auto-updater now works** — native binary installs to `~/.local/bin/claude` owned by the container user, so `claude update` can write freely without root permission issues -- **setup-update-claude.sh** — stripped all npm fallback and `claude install` bootstrap code; now native-binary-only with 60s timeout and transitional npm cleanup -- **setup-aliases.sh** — simplified `_CLAUDE_BIN` resolution to native binary path only (removed npm and `/usr/local/bin` fallbacks) -- **setup.sh** — fixed background update script invocation to capture all output to log file instead of discarding via `&>/dev/null` +- **Claude Code now installs as a native binary** — uses Anthropic's official installer (`https://claude.ai/install.sh`) via new `./features/claude-code-native` feature, replacing the npm-based `ghcr.io/anthropics/devcontainer-features/claude-code:1.0.5` +- **In-session auto-updater now works without root** — native binary at `~/.local/bin/claude` is owned by the container user, so `claude update` succeeds without permission issues + +### Fixed + +#### Claude Code Installation +- **Update script no longer silently discards errors** — background update output now captured to log file instead of being discarded via `&>/dev/null` +- **Update script simplified to native-binary-only** — removed npm fallback and `claude install` bootstrap code; added 60s timeout and transitional npm cleanup +- **Alias resolution simplified** — `_CLAUDE_BIN` now resolves directly to native binary path (removed npm and `/usr/local/bin` fallbacks) #### System Prompt - **`` section** — Updated to document Claude Code native worktree convention (`/.claude/worktrees/`) as the recommended approach alongside the legacy `.worktrees/` convention. Added `EnterWorktree` tool guidance, `.worktreeinclude` file documentation, and path convention comparison table. diff --git a/.devcontainer/features/claude-code-native/install.sh b/.devcontainer/features/claude-code-native/install.sh index 04e091c..fa83a71 100755 --- a/.devcontainer/features/claude-code-native/install.sh +++ b/.devcontainer/features/claude-code-native/install.sh @@ -14,8 +14,9 @@ echo "[claude-code-native] Starting installation..." echo "[claude-code-native] Version: ${VERSION}" # === VALIDATE DEPENDENCIES === -if ! command -v curl &>/dev/null && ! command -v wget &>/dev/null; then - echo "[claude-code-native] ERROR: curl or wget is required" +# The official installer (claude.ai/install.sh) requires curl internally +if ! command -v curl &>/dev/null; then + echo "[claude-code-native] ERROR: curl is required" echo " Ensure common-utils feature is installed first" exit 1 fi @@ -50,11 +51,13 @@ chown -R "${USERNAME}:" "${USER_HOME}/.local/bin" "${USER_HOME}/.local/share/cla # === DETERMINE TARGET === # The official installer accepts: stable, latest, or a specific semver -TARGET="" -if [ "${VERSION}" != "latest" ] && [ "${VERSION}" != "stable" ]; then - TARGET="${VERSION}" -else - TARGET="${VERSION}" +TARGET="${VERSION}" +if [ "${TARGET}" != "latest" ] && [ "${TARGET}" != "stable" ]; then + if ! echo "${TARGET}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "[claude-code-native] ERROR: Invalid version '${TARGET}'" + echo " Use 'latest', 'stable', or a semver (e.g., 2.1.52)" + exit 1 + fi fi # === INSTALL === @@ -67,7 +70,7 @@ echo "[claude-code-native] Downloading official installer..." if [ "${USERNAME}" = "root" ]; then curl -fsSL https://claude.ai/install.sh | sh -s -- "${TARGET}" else - su - "${USERNAME}" -c "curl -fsSL https://claude.ai/install.sh | sh -s -- ${TARGET}" + su - "${USERNAME}" -c "curl -fsSL https://claude.ai/install.sh | sh -s -- \"${TARGET}\"" fi # === VERIFICATION === diff --git a/.devcontainer/features/mcp-qdrant/devcontainer-feature.json b/.devcontainer/features/mcp-qdrant/devcontainer-feature.json index 67c729f..78d9165 100644 --- a/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +++ b/.devcontainer/features/mcp-qdrant/devcontainer-feature.json @@ -50,6 +50,6 @@ "installsAfter": [ "ghcr.io/devcontainers/features/python:1", "ghcr.io/devcontainers/features/common-utils:2", - "ghcr.io/anthropics/devcontainer-features/claude-code:1" + "./features/claude-code-native" ] } diff --git a/.devcontainer/features/notify-hook/devcontainer-feature.json b/.devcontainer/features/notify-hook/devcontainer-feature.json index 1e72f2e..56bc5c5 100644 --- a/.devcontainer/features/notify-hook/devcontainer-feature.json +++ b/.devcontainer/features/notify-hook/devcontainer-feature.json @@ -23,6 +23,6 @@ }, "installsAfter": [ "ghcr.io/devcontainers/features/common-utils:2", - "ghcr.io/anthropics/devcontainer-features/claude-code:1" + "./features/claude-code-native" ] } diff --git a/.devcontainer/scripts/check-setup.sh b/.devcontainer/scripts/check-setup.sh index f13e458..57d8cae 100644 --- a/.devcontainer/scripts/check-setup.sh +++ b/.devcontainer/scripts/check-setup.sh @@ -34,7 +34,7 @@ warn_check() { echo "" echo "Core:" check "Claude Code installed" "command -v claude" -warn_check "Claude native binary" "[ -x ~/.local/bin/claude ] || [ -x /usr/local/bin/claude ]" +warn_check "Claude native binary" "[ -x ~/.local/bin/claude ]" check "cc alias configured" "grep -q 'alias cc=' ~/.bashrc 2>/dev/null || grep -q 'alias cc=' ~/.zshrc 2>/dev/null" check "Config directory exists" "[ -d '${CLAUDE_CONFIG_DIR:-$HOME/.claude}' ]" check "Settings file exists" "[ -f '${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json' ]" diff --git a/.devcontainer/scripts/setup-update-claude.sh b/.devcontainer/scripts/setup-update-claude.sh index 386977c..335e63a 100755 --- a/.devcontainer/scripts/setup-update-claude.sh +++ b/.devcontainer/scripts/setup-update-claude.sh @@ -55,7 +55,9 @@ CURRENT_VERSION=$("$NATIVE_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+ log "Current version: ${CURRENT_VERSION}" # Use the official update command with timeout (handles download, verification, and versioned install) -if timeout 60 "$NATIVE_BIN" update 2>&1 | tee -a "$LOG_FILE"; then +timeout 60 "$NATIVE_BIN" update 2>&1 | tee -a "$LOG_FILE" +UPDATE_STATUS=${PIPESTATUS[0]} +if [ "$UPDATE_STATUS" -eq 0 ]; then UPDATED_VERSION=$("$NATIVE_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown") if [ "$CURRENT_VERSION" != "$UPDATED_VERSION" ]; then log "Updated Claude Code: ${CURRENT_VERSION} → ${UPDATED_VERSION}" @@ -63,5 +65,5 @@ if timeout 60 "$NATIVE_BIN" update 2>&1 | tee -a "$LOG_FILE"; then log "Already up to date (${CURRENT_VERSION})" fi else - log "WARNING: 'claude update' failed or timed out" + log "WARNING: 'claude update' failed or timed out (exit ${UPDATE_STATUS})" fi diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index 4566e68..9d244c3 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -118,7 +118,9 @@ run_script "$SCRIPT_DIR/setup-terminal.sh" "$SETUP_TERMINAL" # Background the update to avoid blocking container start if [ "$SETUP_UPDATE_CLAUDE" = "true" ] && [ -f "$SCRIPT_DIR/setup-update-claude.sh" ]; then - bash "$SCRIPT_DIR/setup-update-claude.sh" >>"${TMPDIR:-/tmp}/claude-update.log" 2>&1 & + CLAUDE_UPDATE_LOG="${CLAUDE_UPDATE_LOG:-/workspaces/.tmp/claude-update.log}" + mkdir -p "$(dirname "$CLAUDE_UPDATE_LOG")" + bash "$SCRIPT_DIR/setup-update-claude.sh" >>"$CLAUDE_UPDATE_LOG" 2>&1 & disown SETUP_RESULTS+=("setup-update-claude:background") else From 1eebfc5b9140784e9b637cee1b3a50ccd4d8564c Mon Sep 17 00:00:00 2001 From: AnExiledDev Date: Thu, 26 Feb 2026 02:42:43 +0000 Subject: [PATCH 3/3] fix(native-installer): POSIX compat, bash installer, onboarding hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace &>/dev/null with >/dev/null 2>&1 (POSIX) - Require curl only (not wget) — matches official installer dependency - Use bash instead of sh for piping installer (it requires bash) - Quote ${TARGET} in su -c to prevent word splitting - Pre-create ~/.local/state and ~/.claude directories - Add 99-claude-onboarding.sh post-start hook to ensure hasCompletedOnboarding is set when token auth is configured --- .devcontainer/CHANGELOG.md | 19 +++++--- .../features/claude-code-native/install.sh | 47 +++++++++++++++++-- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/.devcontainer/CHANGELOG.md b/.devcontainer/CHANGELOG.md index 067c186..eb5e73f 100644 --- a/.devcontainer/CHANGELOG.md +++ b/.devcontainer/CHANGELOG.md @@ -10,19 +10,15 @@ #### Skills - **worktree** — New skill for git worktree creation, management, and cleanup. Covers `EnterWorktree` tool, `--worktree` CLI flag, `.worktreeinclude` setup, worktree naming conventions, cleanup lifecycle, and CodeForge integration (Project Manager auto-detection, agent isolation). Includes two reference files: manual worktree commands and parallel workflow patterns. +#### Claude Code Installation +- **Post-start onboarding hook** (`99-claude-onboarding.sh`) — ensures `hasCompletedOnboarding: true` in `.claude.json` when token auth is configured; catches overwrites from Claude Code CLI/extension that race with `postStartCommand` + ### Changed #### Claude Code Installation - **Claude Code now installs as a native binary** — uses Anthropic's official installer (`https://claude.ai/install.sh`) via new `./features/claude-code-native` feature, replacing the npm-based `ghcr.io/anthropics/devcontainer-features/claude-code:1.0.5` - **In-session auto-updater now works without root** — native binary at `~/.local/bin/claude` is owned by the container user, so `claude update` succeeds without permission issues -### Fixed - -#### Claude Code Installation -- **Update script no longer silently discards errors** — background update output now captured to log file instead of being discarded via `&>/dev/null` -- **Update script simplified to native-binary-only** — removed npm fallback and `claude install` bootstrap code; added 60s timeout and transitional npm cleanup -- **Alias resolution simplified** — `_CLAUDE_BIN` now resolves directly to native binary path (removed npm and `/usr/local/bin` fallbacks) - #### System Prompt - **`` section** — Updated to document Claude Code native worktree convention (`/.claude/worktrees/`) as the recommended approach alongside the legacy `.worktrees/` convention. Added `EnterWorktree` tool guidance, `.worktreeinclude` file documentation, and path convention comparison table. @@ -59,6 +55,15 @@ ### Fixed +#### Claude Code Installation +- **Update script no longer silently discards errors** — background update output now captured to log file instead of being discarded via `&>/dev/null` +- **Update script simplified to native-binary-only** — removed npm fallback and `claude install` bootstrap code; added 60s timeout and transitional npm cleanup +- **Alias resolution simplified** — `_CLAUDE_BIN` now resolves directly to native binary path (removed npm and `/usr/local/bin` fallbacks) +- **POSIX redirect** — replaced `&>/dev/null` with `>/dev/null 2>&1` in dependency check for portability +- **Installer shell** — changed `sh -s` to `bash -s` when piping the official installer (it requires bash) +- **Unquoted `${TARGET}`** — quoted variable in `su -c` command to prevent word splitting +- **Directory prep** — added `~/.local/state` and `~/.claude` pre-creation; consolidated `chown` to cover entire `~/.local` tree + #### Plugin Marketplace - **`marketplace.json` schema fix** — changed all 11 plugin `source` fields from bare names (e.g., `"codeforge-lsp"`) to relative paths (`"./plugins/codeforge-lsp"`) so `claude plugin marketplace add` passes schema validation and all plugins register correctly diff --git a/.devcontainer/features/claude-code-native/install.sh b/.devcontainer/features/claude-code-native/install.sh index fa83a71..d620ca3 100755 --- a/.devcontainer/features/claude-code-native/install.sh +++ b/.devcontainer/features/claude-code-native/install.sh @@ -15,7 +15,7 @@ echo "[claude-code-native] Version: ${VERSION}" # === VALIDATE DEPENDENCIES === # The official installer (claude.ai/install.sh) requires curl internally -if ! command -v curl &>/dev/null; then +if ! command -v curl >/dev/null 2>&1; then echo "[claude-code-native] ERROR: curl is required" echo " Ensure common-utils feature is installed first" exit 1 @@ -47,7 +47,9 @@ echo "[claude-code-native] Installing for user: ${USERNAME} (home: ${USER_HOME}) # === PREPARE DIRECTORIES === mkdir -p "${USER_HOME}/.local/bin" mkdir -p "${USER_HOME}/.local/share/claude" -chown -R "${USERNAME}:" "${USER_HOME}/.local/bin" "${USER_HOME}/.local/share/claude" +mkdir -p "${USER_HOME}/.local/state" +mkdir -p "${USER_HOME}/.claude" +chown -R "${USERNAME}:" "${USER_HOME}/.local" "${USER_HOME}/.claude" # === DETERMINE TARGET === # The official installer accepts: stable, latest, or a specific semver @@ -68,9 +70,9 @@ fi echo "[claude-code-native] Downloading official installer..." if [ "${USERNAME}" = "root" ]; then - curl -fsSL https://claude.ai/install.sh | sh -s -- "${TARGET}" + curl -fsSL https://claude.ai/install.sh | bash -s -- "${TARGET}" else - su - "${USERNAME}" -c "curl -fsSL https://claude.ai/install.sh | sh -s -- \"${TARGET}\"" + su - "${USERNAME}" -c "curl -fsSL https://claude.ai/install.sh | bash -s -- \"${TARGET}\"" fi # === VERIFICATION === @@ -87,4 +89,41 @@ else exit 1 fi +# === POST-START HOOK === +# Ensures hasCompletedOnboarding is set when token auth is configured. +# Runs as the LAST post-start hook (99- prefix) to catch overwrites from +# Claude Code CLI/extension that may race with postStartCommand. +HOOK_DIR="/usr/local/devcontainer-poststart.d" +mkdir -p "$HOOK_DIR" +cat > "$HOOK_DIR/99-claude-onboarding.sh" << 'HOOK_EOF' +#!/bin/bash +# Ensure hasCompletedOnboarding: true in .claude.json when token auth exists. +# Runs after all setup scripts to catch any overwrites by Claude Code CLI/extension. +_USERNAME="${SUDO_USER:-${USER:-vscode}}" +_USER_HOME=$(getent passwd "$_USERNAME" 2>/dev/null | cut -d: -f6) +_USER_HOME="${_USER_HOME:-/home/$_USERNAME}" +CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}" +CLAUDE_JSON="$CLAUDE_DIR/.claude.json" +CRED_FILE="$CLAUDE_DIR/.credentials.json" + +# Only act when token auth is configured +[ -f "$CRED_FILE" ] || exit 0 + +if [ -f "$CLAUDE_JSON" ]; then + if ! grep -q '"hasCompletedOnboarding"' "$CLAUDE_JSON" 2>/dev/null; then + if command -v jq >/dev/null 2>&1; then + jq '. + {"hasCompletedOnboarding": true}' "$CLAUDE_JSON" > "${CLAUDE_JSON}.tmp" && \ + mv "${CLAUDE_JSON}.tmp" "$CLAUDE_JSON" + else + sed -i '$ s/}$/,\n "hasCompletedOnboarding": true\n}/' "$CLAUDE_JSON" + fi + echo "[claude-onboarding] Injected hasCompletedOnboarding into .claude.json" + fi +else + printf '{\n "hasCompletedOnboarding": true\n}\n' > "$CLAUDE_JSON" + echo "[claude-onboarding] Created .claude.json with hasCompletedOnboarding" +fi +HOOK_EOF +chmod +x "$HOOK_DIR/99-claude-onboarding.sh" + echo "[claude-code-native] Installation complete"