Skip to content

Commit 5fa1954

Browse files
committed
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.
1 parent d2ba55e commit 5fa1954

File tree

9 files changed

+199
-40
lines changed

9 files changed

+199
-40
lines changed

.devcontainer/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# CodeForge Devcontainer Changelog
22

3+
## [Unreleased]
4+
5+
### Changed
6+
7+
#### Claude Code Installation
8+
- **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`)
9+
- **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
10+
- **setup-update-claude.sh** — stripped all npm fallback and `claude install` bootstrap code; now native-binary-only with 60s timeout and transitional npm cleanup
11+
- **setup-aliases.sh** — simplified `_CLAUDE_BIN` resolution to native binary path only (removed npm and `/usr/local/bin` fallbacks)
12+
- **setup.sh** — fixed background update script invocation to capture all output to log file instead of discarding via `&>/dev/null`
13+
314
## [v1.14.2] - 2026-02-24
415

516
### Fixed

.devcontainer/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,4 @@ All experimental feature flags are in `settings.json` under `env`. Setup steps c
9494

9595
## Features
9696

97-
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`.
97+
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`).

.devcontainer/devcontainer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
},
3232

3333
// Feature install order: external runtimes first (Node, uv, Rust, Bun),
34-
// then Claude Code (needs Node), then custom features.
34+
// then Claude Code native binary (no Node dependency), then custom features.
3535
// npm-dependent features (agent-browser, ccusage, ccburn, claude-session-dashboard,
3636
// biome, lsp-servers) must come after Node. uv-dependent features (ruff, claude-monitor) must
3737
// come after uv. cargo-dependent features (ccms) must come after Rust.
@@ -43,7 +43,7 @@
4343
"ghcr.io/devcontainers-extra/features/uv",
4444
"ghcr.io/rails/devcontainer/features/bun",
4545
"ghcr.io/devcontainers/features/rust",
46-
"ghcr.io/anthropics/devcontainer-features/claude-code",
46+
"./features/claude-code-native",
4747
"./features/tmux",
4848
"./features/agent-browser",
4949
"./features/claude-monitor",
@@ -82,7 +82,7 @@
8282
},
8383
// Uncomment to add Go runtime (not installed by default):
8484
// "ghcr.io/devcontainers/features/go:1": {},
85-
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0.5": {},
85+
"./features/claude-code-native": {},
8686
"./features/tmux": {},
8787
"./features/ccusage": {
8888
"version": "latest",
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Claude Code CLI (Native Binary)
2+
3+
Installs [Claude Code](https://docs.anthropic.com/en/docs/claude-code) as a native binary using Anthropic's official installer.
4+
5+
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.
6+
7+
## Options
8+
9+
| Option | Default | Description |
10+
|--------|---------|-------------|
11+
| `version` | `latest` | `latest`, `stable`, or a specific semver (e.g., `2.1.52`). Set to `none` to skip. |
12+
| `username` | `automatic` | Container user to install for. `automatic` detects from `$_REMOTE_USER`. |
13+
14+
## How it works
15+
16+
1. Downloads the official installer from `https://claude.ai/install.sh`
17+
2. Runs it as the target user (not root)
18+
3. The installer handles platform detection, checksum verification, and binary placement
19+
4. Binary is installed to `~/.local/bin/claude` with versions stored in `~/.local/share/claude/versions/`
20+
21+
## Usage
22+
23+
```json
24+
{
25+
"features": {
26+
"./features/claude-code-native": {}
27+
}
28+
}
29+
```
30+
31+
With version pinning:
32+
33+
```json
34+
{
35+
"features": {
36+
"./features/claude-code-native": {
37+
"version": "2.1.52"
38+
}
39+
}
40+
}
41+
```
42+
43+
## Why native over npm?
44+
45+
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.
46+
47+
The native binary installs to `~/.local/` under the container user's ownership, so `claude update` works without elevated permissions.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"id": "claude-code-native",
3+
"version": "1.0.0",
4+
"name": "Claude Code CLI (Native Binary)",
5+
"description": "Installs Claude Code CLI as a native binary via the official Anthropic installer",
6+
"documentationURL": "https://docs.anthropic.com/en/docs/claude-code",
7+
"options": {
8+
"version": {
9+
"type": "string",
10+
"description": "Version to install: 'latest', 'stable', or a specific semver. Use 'none' to skip.",
11+
"default": "latest"
12+
},
13+
"username": {
14+
"type": "string",
15+
"description": "Container user to install for",
16+
"default": "automatic"
17+
}
18+
},
19+
"customizations": {
20+
"vscode": {
21+
"extensions": [
22+
"anthropic.claude-code"
23+
]
24+
}
25+
},
26+
"installsAfter": [
27+
"ghcr.io/devcontainers/features/common-utils:2"
28+
]
29+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
VERSION="${VERSION:-latest}"
5+
USERNAME="${USERNAME:-automatic}"
6+
7+
# Skip installation if version is "none"
8+
if [ "${VERSION}" = "none" ]; then
9+
echo "[claude-code-native] Skipping installation (version=none)"
10+
exit 0
11+
fi
12+
13+
echo "[claude-code-native] Starting installation..."
14+
echo "[claude-code-native] Version: ${VERSION}"
15+
16+
# === VALIDATE DEPENDENCIES ===
17+
if ! command -v curl &>/dev/null && ! command -v wget &>/dev/null; then
18+
echo "[claude-code-native] ERROR: curl or wget is required"
19+
echo " Ensure common-utils feature is installed first"
20+
exit 1
21+
fi
22+
23+
# === DETECT USER ===
24+
if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then
25+
if [ -n "${_REMOTE_USER:-}" ]; then
26+
USERNAME="${_REMOTE_USER}"
27+
elif getent passwd vscode >/dev/null 2>&1; then
28+
USERNAME="vscode"
29+
elif getent passwd node >/dev/null 2>&1; then
30+
USERNAME="node"
31+
elif getent passwd codespace >/dev/null 2>&1; then
32+
USERNAME="codespace"
33+
else
34+
USERNAME="root"
35+
fi
36+
fi
37+
38+
USER_HOME=$(getent passwd "${USERNAME}" | cut -d: -f6)
39+
if [ -z "${USER_HOME}" ]; then
40+
echo "[claude-code-native] ERROR: Could not determine home directory for ${USERNAME}"
41+
exit 1
42+
fi
43+
44+
echo "[claude-code-native] Installing for user: ${USERNAME} (home: ${USER_HOME})"
45+
46+
# === PREPARE DIRECTORIES ===
47+
mkdir -p "${USER_HOME}/.local/bin"
48+
mkdir -p "${USER_HOME}/.local/share/claude"
49+
chown -R "${USERNAME}:" "${USER_HOME}/.local/bin" "${USER_HOME}/.local/share/claude"
50+
51+
# === DETERMINE TARGET ===
52+
# The official installer accepts: stable, latest, or a specific semver
53+
TARGET=""
54+
if [ "${VERSION}" != "latest" ] && [ "${VERSION}" != "stable" ]; then
55+
TARGET="${VERSION}"
56+
else
57+
TARGET="${VERSION}"
58+
fi
59+
60+
# === INSTALL ===
61+
# The official Anthropic installer handles:
62+
# - Platform detection (linux/darwin, x64/arm64, glibc/musl)
63+
# - Manifest download and checksum verification
64+
# - Binary download to ~/.local/bin/claude (symlink to ~/.local/share/claude/versions/*)
65+
echo "[claude-code-native] Downloading official installer..."
66+
67+
if [ "${USERNAME}" = "root" ]; then
68+
curl -fsSL https://claude.ai/install.sh | sh -s -- "${TARGET}"
69+
else
70+
su - "${USERNAME}" -c "curl -fsSL https://claude.ai/install.sh | sh -s -- ${TARGET}"
71+
fi
72+
73+
# === VERIFICATION ===
74+
CLAUDE_BIN="${USER_HOME}/.local/bin/claude"
75+
76+
if [ -x "${CLAUDE_BIN}" ]; then
77+
INSTALLED_VERSION=$(su - "${USERNAME}" -c "${CLAUDE_BIN} --version 2>/dev/null" || echo "unknown")
78+
echo "[claude-code-native] ✓ Claude Code installed: ${INSTALLED_VERSION}"
79+
echo "[claude-code-native] Binary: ${CLAUDE_BIN}"
80+
else
81+
echo "[claude-code-native] ERROR: Installation failed — ${CLAUDE_BIN} not found or not executable"
82+
echo "[claude-code-native] Expected binary at: ${CLAUDE_BIN}"
83+
ls -la "${USER_HOME}/.local/bin/" 2>/dev/null || true
84+
exit 1
85+
fi
86+
87+
echo "[claude-code-native] Installation complete"

.devcontainer/scripts/setup-aliases.sh

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,8 @@ export GH_CONFIG_DIR="${GH_CONFIG_DIR:-/workspaces/.gh}"
7474
export LANG=en_US.UTF-8
7575
export LC_ALL=en_US.UTF-8
7676
77-
# Prefer native binary over npm-installed version
78-
if [ -x "\$HOME/.local/bin/claude" ]; then
79-
_CLAUDE_BIN="\$HOME/.local/bin/claude"
80-
elif [ -x /usr/local/bin/claude ]; then
81-
_CLAUDE_BIN=/usr/local/bin/claude
82-
else
83-
_CLAUDE_BIN=claude
84-
fi
77+
# Native binary (installed by claude-code-native feature)
78+
_CLAUDE_BIN="\$HOME/.local/bin/claude"
8579
8680
# ChromaTerm wrapper (if ct is installed, wrap claude through it)
8781
if command -v ct >/dev/null 2>&1; then

.devcontainer/scripts/setup-update-claude.sh

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,52 +25,43 @@ fi
2525

2626
# === CLEANUP TRAP ===
2727
cleanup() {
28-
rm -f "${_TMPDIR}/claude-update" 2>/dev/null || true
29-
rm -f "${_TMPDIR}/claude-update-manifest.json" 2>/dev/null || true
3028
rm -rf "$LOCK_FILE" 2>/dev/null || true
3129
}
3230
trap cleanup EXIT
3331

34-
# === VERIFY CLAUDE IS INSTALLED ===
35-
if ! command -v claude &>/dev/null; then
36-
log "Claude Code not found, skipping update"
37-
exit 0
38-
fi
32+
# === NATIVE BINARY ===
33+
NATIVE_BIN="$HOME/.local/bin/claude"
3934

40-
# === ENSURE NATIVE BINARY EXISTS ===
41-
# 'claude install' puts the binary at ~/.local/bin/claude (symlink to ~/.local/share/claude/versions/*)
42-
# Legacy manual installs used /usr/local/bin/claude — check both, prefer ~/.local
43-
if [ -x "$HOME/.local/bin/claude" ]; then
44-
NATIVE_BIN="$HOME/.local/bin/claude"
45-
elif [ -x "/usr/local/bin/claude" ]; then
46-
NATIVE_BIN="/usr/local/bin/claude"
47-
else
48-
NATIVE_BIN=""
35+
if [ ! -x "$NATIVE_BIN" ]; then
36+
log "ERROR: Native binary not found at ${NATIVE_BIN}"
37+
log " The claude-code-native feature should install this during container build."
38+
log " Try rebuilding the container or running: curl -fsSL https://claude.ai/install.sh | sh"
39+
exit 1
4940
fi
50-
if [ -z "$NATIVE_BIN" ]; then
51-
log "Native binary not found, installing..."
52-
if claude install 2>&1 | tee -a "$LOG_FILE"; then
53-
log "Native binary installed successfully"
41+
42+
# === TRANSITIONAL: Remove leftover npm installation ===
43+
NPM_CLAUDE="$(npm config get prefix 2>/dev/null)/lib/node_modules/@anthropic-ai/claude-code"
44+
if [ -d "$NPM_CLAUDE" ]; then
45+
log "Removing leftover npm installation at ${NPM_CLAUDE}..."
46+
if sudo npm uninstall -g @anthropic-ai/claude-code 2>/dev/null; then
47+
log "Removed leftover npm installation"
5448
else
55-
log "WARNING: 'claude install' failed, skipping"
56-
exit 0
49+
log "WARNING: Could not remove npm installation (non-blocking)"
5750
fi
58-
# Skip update check on first install — next start will handle it
59-
exit 0
6051
fi
6152

6253
# === CHECK FOR UPDATES ===
6354
CURRENT_VERSION=$("$NATIVE_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
6455
log "Current version: ${CURRENT_VERSION}"
6556

66-
# Use the official update command (handles download, verification, and versioned install)
67-
if "$NATIVE_BIN" update 2>&1 | tee -a "$LOG_FILE"; then
57+
# Use the official update command with timeout (handles download, verification, and versioned install)
58+
if timeout 60 "$NATIVE_BIN" update 2>&1 | tee -a "$LOG_FILE"; then
6859
UPDATED_VERSION=$("$NATIVE_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
6960
if [ "$CURRENT_VERSION" != "$UPDATED_VERSION" ]; then
7061
log "Updated Claude Code: ${CURRENT_VERSION}${UPDATED_VERSION}"
7162
else
7263
log "Already up to date (${CURRENT_VERSION})"
7364
fi
7465
else
75-
log "WARNING: 'claude update' failed, skipping"
66+
log "WARNING: 'claude update' failed or timed out"
7667
fi

.devcontainer/scripts/setup.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ run_script "$SCRIPT_DIR/setup-terminal.sh" "$SETUP_TERMINAL"
9898

9999
# Background the update to avoid blocking container start
100100
if [ "$SETUP_UPDATE_CLAUDE" = "true" ] && [ -f "$SCRIPT_DIR/setup-update-claude.sh" ]; then
101-
bash "$SCRIPT_DIR/setup-update-claude.sh" &>/dev/null &
101+
bash "$SCRIPT_DIR/setup-update-claude.sh" >>"${TMPDIR:-/tmp}/claude-update.log" 2>&1 &
102102
disown
103103
SETUP_RESULTS+=("setup-update-claude:background")
104104
else

0 commit comments

Comments
 (0)