Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .devcontainer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- **`<git_worktrees>` section** — Updated to document Claude Code native worktree convention (`<repo>/.claude/worktrees/`) as the recommended approach alongside the legacy `.worktrees/` convention. Added `EnterWorktree` tool guidance, `.worktreeinclude` file documentation, and path convention comparison table.

Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
6 changes: 3 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
47 changes: 47 additions & 0 deletions .devcontainer/features/claude-code-native/README.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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"
]
}
129 changes: 129 additions & 0 deletions .devcontainer/features/claude-code-native/install.sh
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +112 to +121
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure onboarding is forced to true, not just present.

Current logic only patches when the key is missing. If .claude.json already has "hasCompletedOnboarding": false, it will remain false.

🔧 Suggested fix
 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
+    if command -v jq >/dev/null 2>&1; then
+        jq '.hasCompletedOnboarding = true' "$CLAUDE_JSON" > "${CLAUDE_JSON}.tmp" && \
+            mv "${CLAUDE_JSON}.tmp" "$CLAUDE_JSON"
+        echo "[claude-onboarding] Ensured hasCompletedOnboarding=true in .claude.json"
+    else
+        if grep -q '"hasCompletedOnboarding"[[:space:]]*:[[:space:]]*false' "$CLAUDE_JSON" 2>/dev/null; then
+            sed -i 's/"hasCompletedOnboarding"[[:space:]]*:[[:space:]]*false/"hasCompletedOnboarding": true/' "$CLAUDE_JSON"
+            echo "[claude-onboarding] Updated hasCompletedOnboarding to true in .claude.json"
+        elif ! grep -q '"hasCompletedOnboarding"' "$CLAUDE_JSON" 2>/dev/null; then
+            sed -i '$ s/}$/,\n  "hasCompletedOnboarding": true\n}/' "$CLAUDE_JSON"
+            echo "[claude-onboarding] Injected hasCompletedOnboarding into .claude.json"
+        fi
+    fi
 else
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.devcontainer/features/claude-code-native/install.sh around lines 112 - 121,
The current block only injects hasCompletedOnboarding when missing; change it to
unconditionally set hasCompletedOnboarding to true in the JSON file referenced
by CLAUDE_JSON: if jq is available, use jq to assign .hasCompletedOnboarding =
true (overwrite existing value) and atomically mv the temp file back to
CLAUDE_JSON; otherwise use sed to replace any existing "hasCompletedOnboarding":
... entry with "hasCompletedOnboarding": true or append the field if missing.
Keep references to CLAUDE_JSON, jq, sed and ensure the script logs the same echo
after forcing the value.

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"
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
},
"installsAfter": [
"ghcr.io/devcontainers/features/common-utils:2",
"ghcr.io/anthropics/devcontainer-features/claude-code:1"
"./features/claude-code-native"
]
}
2 changes: 1 addition & 1 deletion .devcontainer/scripts/check-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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' ]"
Expand Down
10 changes: 2 additions & 8 deletions .devcontainer/scripts/setup-aliases.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 20 additions & 27 deletions .devcontainer/scripts/setup-update-claude.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,52 +25,45 @@ 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}"
else
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
4 changes: 3 additions & 1 deletion .devcontainer/scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down