Replace npm Claude Code with native binary installation#11
Conversation
📝 WalkthroughWalkthroughMigrates Claude Code installation to a native binary using Anthropic's official installer, adds a new devcontainer feature and install script, updates devcontainer configuration and feature references, and adjusts setup/update scripts and documentation to use the native binary path and logging. Changes
Sequence DiagramsequenceDiagram
participant Devcontainer as Devcontainer Build
participant Feature as claude-code-native Feature
participant Script as install.sh
participant System as Host/Container
participant Installer as Anthropic Installer
participant Claude as claude Binary
Devcontainer->>Feature: run feature install
Feature->>Script: invoke install.sh
Script->>System: detect target user & home
Script->>System: create ~/.local/bin and data dirs
Script->>Installer: download & execute official installer
Installer->>System: place binary and verify checksum
System->>Claude: write binary to ~/.local/bin/claude
Script->>Claude: verify executable and query version
Script->>Devcontainer: report success or error
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
.devcontainer/scripts/setup.sh (1)
101-101: LGTM — log capture fix is correct.Redirecting both streams to the log file rather than
/dev/nulldirectly addresses the stated goal and is a straightforward improvement.One thing worth keeping in mind: with
>>(append) mode, ifTMPDIRpoints to a persistent path (e.g., a workspace-mounted directory), the log grows unboundedly across container restarts with no rotation. Additionally, whenTMPDIRis unset the log resolves to/tmp/claude-update.log, which diverges from/workspaces/.tmp/claude-update.logreferenced in the PR test plan — testers may not find the log where expected.💡 Optional: pin the log to a stable, discoverable path
- 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 &This makes the log location explicit, consistent with the test plan, and easily overridable via
.env.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.devcontainer/scripts/setup.sh at line 101, Replace the current background invocation that uses TMPDIR default ("bash \"$SCRIPT_DIR/setup-update-claude.sh\" >>\"${TMPDIR:-/tmp}/claude-update.log\" 2>&1 &") with a pinned, discoverable log path (e.g. use a dedicated env var like CLAUDE_LOG or default to /workspaces/.tmp/claude-update.log) so testers can reliably find the log; keep the redirection of both stdout/stderr and backgrounding, ensure you still reference SCRIPT_DIR when launching setup-update-claude.sh, and allow overriding the log location via environment variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.devcontainer/CHANGELOG.md:
- Around line 8-13: Rewrite the changelog block into user-facing outcome bullets
and move implementation/fix details under a new "### Fixed" section: describe
user-visible changes for the native-claude feature (e.g., "Claude now installed
via official native installer; in-session updates work without root" and
"background updates now log output"), then under "### Fixed" list the specific
fixes referencing the scripts and items by name (setup-update-claude.sh,
setup-aliases.sh, setup.sh, and the new ./features/claude-code-native feature),
converting lines that are currently implementation-centric (npm fallback
removal, timeout, alias resolution changes, logging fix) into brief fix entries
rather than technical how-to statements.
In @.devcontainer/features/claude-code-native/install.sh:
- Around line 17-21: The install script currently validates that either curl or
wget exists but later always calls curl (lines invoking curl) and leaves
${TARGET} unquoted in the su invocation; update the install logic to detect
which downloader is available (prefer curl then wget), store that choice in a
variable (e.g., DOWNLOADER) and use it for both download invocations instead of
hard-coding curl, and ensure the su command uses a quoted "${TARGET}" everywhere
to avoid word-splitting/injection; adjust references in the script to use
DOWNLOADER and quoted "${TARGET}" (and handle wget flags/behavior as needed) so
wget-only environments work and quoting is consistent.
- Around line 54-58: The script currently uses unvalidated ${VERSION} to set
TARGET and then injects TARGET unquoted into the non-root su command (su -
"${USERNAME}" -c "... sh -s -- ${TARGET}"), risking command injection; update
the logic that sets TARGET (based on VERSION/allowed values) to validate VERSION
against a safe whitelist or regex (e.g., only alphanumerics, dots, hyphens or
specifically allow "latest"/"stable") and fail fast on invalid values, and
ensure TARGET is always quoted when passed into commands invoked as another user
(change the su - "${USERNAME}" -c invocation to use "${TARGET}" instead of
${TARGET}); reference symbols: VERSION, TARGET, USERNAME and the su - ... -c
"... sh -s -- ${TARGET}" invocation.
In @.devcontainer/scripts/setup-update-claude.sh:
- Around line 57-66: The pipeline using timeout and tee checks only tee's exit
status, so failed or timed-out updates are treated as success; enable pipe
failure handling by either setting "set -o pipefail" before the pipeline or
explicitly capture the pipeline exit code via PIPESTATUS (e.g., check
PIPESTATUS[0] after running timeout "$NATIVE_BIN" update | tee -a "$LOG_FILE")
and use that exit code to decide success/failure when determining
UPDATED_VERSION and logging; update the conditional that references NATIVE_BIN,
CURRENT_VERSION, UPDATED_VERSION, and LOG_FILE to rely on the real
timeout/update exit code rather than tee's exit status.
---
Nitpick comments:
In @.devcontainer/scripts/setup.sh:
- Line 101: Replace the current background invocation that uses TMPDIR default
("bash \"$SCRIPT_DIR/setup-update-claude.sh\"
>>\"${TMPDIR:-/tmp}/claude-update.log\" 2>&1 &") with a pinned, discoverable log
path (e.g. use a dedicated env var like CLAUDE_LOG or default to
/workspaces/.tmp/claude-update.log) so testers can reliably find the log; keep
the redirection of both stdout/stderr and backgrounding, ensure you still
reference SCRIPT_DIR when launching setup-update-claude.sh, and allow overriding
the log location via environment variable.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
.devcontainer/CHANGELOG.md.devcontainer/CLAUDE.md.devcontainer/devcontainer.json.devcontainer/features/claude-code-native/README.md.devcontainer/features/claude-code-native/devcontainer-feature.json.devcontainer/features/claude-code-native/install.sh.devcontainer/scripts/setup-aliases.sh.devcontainer/scripts/setup-update-claude.sh.devcontainer/scripts/setup.sh
| 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 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n .devcontainer/features/claude-code-native/install.shRepository: AnExiledDev/CodeForge
Length of output: 3481
Dependency check accepts wget, but installation code hard-requires curl.
Lines 17–21 validate that either curl or wget is available. However, both lines 68 and 70 unconditionally invoke curl, causing failure in wget-only environments despite passing validation.
Additionally, line 70 leaves ${TARGET} unquoted within the su command, creating an inconsistency with line 68 (which quotes it). If ${TARGET} contains spaces or special characters, this causes argument splitting or injection issues.
Proposed fix: detect available downloader and use it consistently. Also quote ${TARGET} on line 70 to match line 68.
🤖 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 17 - 21,
The install script currently validates that either curl or wget exists but later
always calls curl (lines invoking curl) and leaves ${TARGET} unquoted in the su
invocation; update the install logic to detect which downloader is available
(prefer curl then wget), store that choice in a variable (e.g., DOWNLOADER) and
use it for both download invocations instead of hard-coding curl, and ensure the
su command uses a quoted "${TARGET}" everywhere to avoid
word-splitting/injection; adjust references in the script to use DOWNLOADER and
quoted "${TARGET}" (and handle wget flags/behavior as needed) so wget-only
environments work and quoting is consistent.
Bug:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.devcontainer/features/claude-code-native/install.sh:
- Around line 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.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
.devcontainer/CHANGELOG.md.devcontainer/features/claude-code-native/install.sh.devcontainer/features/mcp-qdrant/devcontainer-feature.json.devcontainer/features/notify-hook/devcontainer-feature.json.devcontainer/scripts/check-setup.sh.devcontainer/scripts/setup-update-claude.sh.devcontainer/scripts/setup.sh
✅ Files skipped from review due to trivial changes (1)
- .devcontainer/CHANGELOG.md
| 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 |
There was a problem hiding this comment.
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.
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.
- 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
- 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
9bd6e65 to
1eebfc5
Compare
Summary
ghcr.io/anthropics/devcontainer-features/claude-code:1.0.5(npm-based) with new./features/claude-code-nativefeature using Anthropic's official native installer (https://claude.ai/install.sh)~/.local/bin/claudeis owned by the container user, soclaude updateworks without root permission issuessetup-update-claude.shto native-binary-only with 60s timeout and transitional npm cleanupsetup-aliases.sh— single_CLAUDE_BINpath instead of 3-branch fallbacksetup.shlog capture — unhandled errors now reach the update log instead of being discardedRoot cause
The Anthropic devcontainer feature installed Claude Code via
npm install -gas root during build. The resulting package at/usr/local/share/nvm/versions/node/*/lib/node_modules/@anthropic-ai/claude-code/was owned byroot:nvmwith2755permissions. Thevscodeuser (innvmgroup) had read+execute but no write, so the in-session auto-updater failed with EACCES on every update attempt.Test plan
~/.local/bin/claudeexists, owned byvscode:vscodenpm list -g @anthropic-ai/claude-codeshows emptycc --versionandclaude --versionresolve to native binarybash .devcontainer/scripts/setup-update-claude.sh— check log at/workspaces/.tmp/claude-update.logSummary by CodeRabbit
Documentation
Chores
Bug Fixes