diff --git a/.devcontainer/CHANGELOG.md b/.devcontainer/CHANGELOG.md index 29581b4..eb5e73f 100644 --- a/.devcontainer/CHANGELOG.md +++ b/.devcontainer/CHANGELOG.md @@ -10,8 +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 + #### 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. @@ -48,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/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..d620ca3 --- /dev/null +++ b/.devcontainer/features/claude-code-native/install.sh @@ -0,0 +1,129 @@ +#!/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 === +# The official installer (claude.ai/install.sh) requires curl internally +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 +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" +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 +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 === +# 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 | bash -s -- "${TARGET}" +else + su - "${USERNAME}" -c "curl -fsSL https://claude.ai/install.sh | bash -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 + +# === 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" 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-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..335e63a 100755 --- a/.devcontainer/scripts/setup-update-claude.sh +++ b/.devcontainer/scripts/setup-update-claude.sh @@ -25,46 +25,39 @@ 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) +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}" @@ -72,5 +65,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 (exit ${UPDATE_STATUS})" fi diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index d865c67..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" &>/dev/null & + 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