Skip to content

Commit 8e0f1fd

Browse files
authored
Replace npm Claude Code with native binary installation (#11)
* 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. * 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 * fix(native-installer): POSIX compat, bash installer, onboarding hook - 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 --------- Co-authored-by: AnExiledDev <AnExiledDev@users.noreply.github.com>
1 parent 104da7f commit 8e0f1fd

File tree

12 files changed

+253
-43
lines changed

12 files changed

+253
-43
lines changed

.devcontainer/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@
1010
#### Skills
1111
- **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.
1212

13+
#### Claude Code Installation
14+
- **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`
15+
1316
### Changed
1417

18+
#### Claude Code Installation
19+
- **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`
20+
- **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
21+
1522
#### System Prompt
1623
- **`<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.
1724

@@ -48,6 +55,15 @@
4855

4956
### Fixed
5057

58+
#### Claude Code Installation
59+
- **Update script no longer silently discards errors** — background update output now captured to log file instead of being discarded via `&>/dev/null`
60+
- **Update script simplified to native-binary-only** — removed npm fallback and `claude install` bootstrap code; added 60s timeout and transitional npm cleanup
61+
- **Alias resolution simplified**`_CLAUDE_BIN` now resolves directly to native binary path (removed npm and `/usr/local/bin` fallbacks)
62+
- **POSIX redirect** — replaced `&>/dev/null` with `>/dev/null 2>&1` in dependency check for portability
63+
- **Installer shell** — changed `sh -s` to `bash -s` when piping the official installer (it requires bash)
64+
- **Unquoted `${TARGET}`** — quoted variable in `su -c` command to prevent word splitting
65+
- **Directory prep** — added `~/.local/state` and `~/.claude` pre-creation; consolidated `chown` to cover entire `~/.local` tree
66+
5167
#### Plugin Marketplace
5268
- **`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
5369

.devcontainer/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,4 @@ Labels are `custom-text` widgets with `merge: "no-padding"` so they fuse visuall
173173

174174
## Features
175175

176-
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`.
176+
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
@@ -45,7 +45,7 @@
4545
},
4646

4747
// Feature install order: external runtimes first (Node, uv, Rust, Bun),
48-
// then Claude Code (needs Node), then custom features.
48+
// then Claude Code native binary (no Node dependency), then custom features.
4949
// npm-dependent features (agent-browser, ccusage, ccburn, claude-session-dashboard,
5050
// biome, lsp-servers) must come after Node. uv-dependent features (ruff, claude-monitor) must
5151
// come after uv. cargo-dependent features (ccms) must come after Rust.
@@ -57,7 +57,7 @@
5757
"ghcr.io/devcontainers-extra/features/uv",
5858
"ghcr.io/rails/devcontainer/features/bun",
5959
"ghcr.io/devcontainers/features/rust",
60-
"ghcr.io/anthropics/devcontainer-features/claude-code",
60+
"./features/claude-code-native",
6161
"./features/tmux",
6262
"./features/agent-browser",
6363
"./features/claude-monitor",
@@ -96,7 +96,7 @@
9696
},
9797
// Uncomment to add Go runtime (not installed by default):
9898
// "ghcr.io/devcontainers/features/go:1": {},
99-
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0.5": {},
99+
"./features/claude-code-native": {},
100100
"./features/tmux": {},
101101
"./features/ccusage": {
102102
"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: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
# The official installer (claude.ai/install.sh) requires curl internally
18+
if ! command -v curl >/dev/null 2>&1; then
19+
echo "[claude-code-native] ERROR: curl is required"
20+
echo " Ensure common-utils feature is installed first"
21+
exit 1
22+
fi
23+
24+
# === DETECT USER ===
25+
if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then
26+
if [ -n "${_REMOTE_USER:-}" ]; then
27+
USERNAME="${_REMOTE_USER}"
28+
elif getent passwd vscode >/dev/null 2>&1; then
29+
USERNAME="vscode"
30+
elif getent passwd node >/dev/null 2>&1; then
31+
USERNAME="node"
32+
elif getent passwd codespace >/dev/null 2>&1; then
33+
USERNAME="codespace"
34+
else
35+
USERNAME="root"
36+
fi
37+
fi
38+
39+
USER_HOME=$(getent passwd "${USERNAME}" | cut -d: -f6)
40+
if [ -z "${USER_HOME}" ]; then
41+
echo "[claude-code-native] ERROR: Could not determine home directory for ${USERNAME}"
42+
exit 1
43+
fi
44+
45+
echo "[claude-code-native] Installing for user: ${USERNAME} (home: ${USER_HOME})"
46+
47+
# === PREPARE DIRECTORIES ===
48+
mkdir -p "${USER_HOME}/.local/bin"
49+
mkdir -p "${USER_HOME}/.local/share/claude"
50+
mkdir -p "${USER_HOME}/.local/state"
51+
mkdir -p "${USER_HOME}/.claude"
52+
chown -R "${USERNAME}:" "${USER_HOME}/.local" "${USER_HOME}/.claude"
53+
54+
# === DETERMINE TARGET ===
55+
# The official installer accepts: stable, latest, or a specific semver
56+
TARGET="${VERSION}"
57+
if [ "${TARGET}" != "latest" ] && [ "${TARGET}" != "stable" ]; then
58+
if ! echo "${TARGET}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
59+
echo "[claude-code-native] ERROR: Invalid version '${TARGET}'"
60+
echo " Use 'latest', 'stable', or a semver (e.g., 2.1.52)"
61+
exit 1
62+
fi
63+
fi
64+
65+
# === INSTALL ===
66+
# The official Anthropic installer handles:
67+
# - Platform detection (linux/darwin, x64/arm64, glibc/musl)
68+
# - Manifest download and checksum verification
69+
# - Binary download to ~/.local/bin/claude (symlink to ~/.local/share/claude/versions/*)
70+
echo "[claude-code-native] Downloading official installer..."
71+
72+
if [ "${USERNAME}" = "root" ]; then
73+
curl -fsSL https://claude.ai/install.sh | bash -s -- "${TARGET}"
74+
else
75+
su - "${USERNAME}" -c "curl -fsSL https://claude.ai/install.sh | bash -s -- \"${TARGET}\""
76+
fi
77+
78+
# === VERIFICATION ===
79+
CLAUDE_BIN="${USER_HOME}/.local/bin/claude"
80+
81+
if [ -x "${CLAUDE_BIN}" ]; then
82+
INSTALLED_VERSION=$(su - "${USERNAME}" -c "${CLAUDE_BIN} --version 2>/dev/null" || echo "unknown")
83+
echo "[claude-code-native] ✓ Claude Code installed: ${INSTALLED_VERSION}"
84+
echo "[claude-code-native] Binary: ${CLAUDE_BIN}"
85+
else
86+
echo "[claude-code-native] ERROR: Installation failed — ${CLAUDE_BIN} not found or not executable"
87+
echo "[claude-code-native] Expected binary at: ${CLAUDE_BIN}"
88+
ls -la "${USER_HOME}/.local/bin/" 2>/dev/null || true
89+
exit 1
90+
fi
91+
92+
# === POST-START HOOK ===
93+
# Ensures hasCompletedOnboarding is set when token auth is configured.
94+
# Runs as the LAST post-start hook (99- prefix) to catch overwrites from
95+
# Claude Code CLI/extension that may race with postStartCommand.
96+
HOOK_DIR="/usr/local/devcontainer-poststart.d"
97+
mkdir -p "$HOOK_DIR"
98+
cat > "$HOOK_DIR/99-claude-onboarding.sh" << 'HOOK_EOF'
99+
#!/bin/bash
100+
# Ensure hasCompletedOnboarding: true in .claude.json when token auth exists.
101+
# Runs after all setup scripts to catch any overwrites by Claude Code CLI/extension.
102+
_USERNAME="${SUDO_USER:-${USER:-vscode}}"
103+
_USER_HOME=$(getent passwd "$_USERNAME" 2>/dev/null | cut -d: -f6)
104+
_USER_HOME="${_USER_HOME:-/home/$_USERNAME}"
105+
CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}"
106+
CLAUDE_JSON="$CLAUDE_DIR/.claude.json"
107+
CRED_FILE="$CLAUDE_DIR/.credentials.json"
108+
109+
# Only act when token auth is configured
110+
[ -f "$CRED_FILE" ] || exit 0
111+
112+
if [ -f "$CLAUDE_JSON" ]; then
113+
if ! grep -q '"hasCompletedOnboarding"' "$CLAUDE_JSON" 2>/dev/null; then
114+
if command -v jq >/dev/null 2>&1; then
115+
jq '. + {"hasCompletedOnboarding": true}' "$CLAUDE_JSON" > "${CLAUDE_JSON}.tmp" && \
116+
mv "${CLAUDE_JSON}.tmp" "$CLAUDE_JSON"
117+
else
118+
sed -i '$ s/}$/,\n "hasCompletedOnboarding": true\n}/' "$CLAUDE_JSON"
119+
fi
120+
echo "[claude-onboarding] Injected hasCompletedOnboarding into .claude.json"
121+
fi
122+
else
123+
printf '{\n "hasCompletedOnboarding": true\n}\n' > "$CLAUDE_JSON"
124+
echo "[claude-onboarding] Created .claude.json with hasCompletedOnboarding"
125+
fi
126+
HOOK_EOF
127+
chmod +x "$HOOK_DIR/99-claude-onboarding.sh"
128+
129+
echo "[claude-code-native] Installation complete"

.devcontainer/features/mcp-qdrant/devcontainer-feature.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,6 @@
5050
"installsAfter": [
5151
"ghcr.io/devcontainers/features/python:1",
5252
"ghcr.io/devcontainers/features/common-utils:2",
53-
"ghcr.io/anthropics/devcontainer-features/claude-code:1"
53+
"./features/claude-code-native"
5454
]
5555
}

.devcontainer/features/notify-hook/devcontainer-feature.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@
2323
},
2424
"installsAfter": [
2525
"ghcr.io/devcontainers/features/common-utils:2",
26-
"ghcr.io/anthropics/devcontainer-features/claude-code:1"
26+
"./features/claude-code-native"
2727
]
2828
}

.devcontainer/scripts/check-setup.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ warn_check() {
3434
echo ""
3535
echo "Core:"
3636
check "Claude Code installed" "command -v claude"
37-
warn_check "Claude native binary" "[ -x ~/.local/bin/claude ] || [ -x /usr/local/bin/claude ]"
37+
warn_check "Claude native binary" "[ -x ~/.local/bin/claude ]"
3838
check "cc alias configured" "grep -q 'alias cc=' ~/.bashrc 2>/dev/null || grep -q 'alias cc=' ~/.zshrc 2>/dev/null"
3939
check "Config directory exists" "[ -d '${CLAUDE_CONFIG_DIR:-$HOME/.claude}' ]"
4040
check "Settings file exists" "[ -f '${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json' ]"

.devcontainer/scripts/setup-aliases.sh

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,8 @@ if [ "\$TERM" = "xterm" ] || [ -z "\$TERM" ]; then
8080
fi
8181
export COLORTERM="\${COLORTERM:-truecolor}"
8282
83-
# Prefer native binary over npm-installed version
84-
if [ -x "\$HOME/.local/bin/claude" ]; then
85-
_CLAUDE_BIN="\$HOME/.local/bin/claude"
86-
elif [ -x /usr/local/bin/claude ]; then
87-
_CLAUDE_BIN=/usr/local/bin/claude
88-
else
89-
_CLAUDE_BIN=claude
90-
fi
83+
# Native binary (installed by claude-code-native feature)
84+
_CLAUDE_BIN="\$HOME/.local/bin/claude"
9185
9286
# ChromaTerm wrapper (if ct is installed, wrap claude through it)
9387
if command -v ct >/dev/null 2>&1; then

0 commit comments

Comments
 (0)