Skip to content

Commit e9fa5bf

Browse files
blooopclaude
andcommitted
fix: make devcontainer portable across hosts
- Add init-host.sh to create ~/.claude directory on host before container starts - Use bind mount for ~/.claude folder instead of individual file mounts - Switch from npm to pixi for claude installation (claude-shim package) - Remove Node.js dependency from claude-code feature - Add pixi bin paths to .profile for proper PATH setup - Add initializeCommand to devcontainer.json to run host init script - Install pixi automatically if not found in base image - Feature now adds PATH setup to user profile (self-contained) This allows the devcontainer to work on fresh hosts without requiring pre-existing ~/.claude files, while still sharing credentials across all devcontainers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a989caf commit e9fa5bf

File tree

5 files changed

+76
-73
lines changed

5 files changed

+76
-73
lines changed

.devcontainer/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ RUN curl -L -o /usr/local/bin/pixi -fsSL --compressed "https://github.com/prefix
1010
USER vscode
1111
WORKDIR /home/vscode
1212

13-
RUN echo 'eval "$(pixi completion -s bash)"' >> /home/vscode/.bashrc
13+
RUN echo 'eval "$(pixi completion -s bash)"' >> /home/vscode/.bashrc \
14+
&& echo 'export PATH="$HOME/.pixi/envs/claude-shim/bin:$HOME/.pixi/bin:$PATH"' >> /home/vscode/.profile
1415

1516
# Create .ssh directory with proper permissions for SSH config mounts
1617
RUN mkdir -p /home/vscode/.ssh && chmod 700 /home/vscode/.ssh
Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"name": "Claude Code CLI",
33
"id": "claude-code",
4-
"version": "0.1.0",
5-
"description": "Installs Claude Code CLI globally and mounts configuration directories",
4+
"version": "0.2.0",
5+
"description": "Installs Claude Code CLI via pixi with persistent configuration",
66
"options": {},
77
"documentationURL": "https://github.com/anthropics/devcontainer-features",
88
"licenseURL": "https://github.com/anthropics/devcontainer-features/blob/main/LICENSE",
@@ -16,16 +16,7 @@
1616
"containerEnv": {
1717
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude"
1818
},
19-
"dependsOn": {
20-
"ghcr.io/devcontainers/features/node": {}
21-
},
2219
"mounts": [
23-
"source=${localEnv:HOME}/.claude/CLAUDE.md,target=/home/vscode/.claude/CLAUDE.md,type=bind,ro",
24-
"source=${localEnv:HOME}/.claude/settings.json,target=/home/vscode/.claude/settings.json,type=bind,ro",
25-
"source=${localEnv:HOME}/.claude/.credentials.json,target=/home/vscode/.claude/.credentials.json,type=bind",
26-
"source=${localEnv:HOME}/.claude/.claude.json,target=/home/vscode/.claude/.claude.json,type=bind",
27-
"source=${localEnv:HOME}/.claude/agents,target=/home/vscode/.claude/agents,type=bind,ro",
28-
"source=${localEnv:HOME}/.claude/commands,target=/home/vscode/.claude/commands,type=bind,ro",
29-
"source=${localEnv:HOME}/.claude/hooks,target=/home/vscode/.claude/hooks,type=bind,ro"
20+
"source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind"
3021
]
3122
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/sh
2+
# Initialize Claude Code host directory for devcontainer bind mount
3+
# This script runs on the HOST before the container is created.
4+
# mkdir -p is idempotent - it only creates if missing, won't clobber existing.
5+
6+
mkdir -p "$HOME/.claude"

.devcontainer/claude-code/install.sh

Lines changed: 64 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,80 @@
22
set -eu
33

44
# Claude Code CLI Local Feature Install Script
5-
# Based on: https://github.com/anthropics/devcontainer-features/pull/25
6-
# Combines CLI installation with configuration directory setup
5+
# Installs Claude Code via pixi and sets up configuration directories
76

8-
# Function to install Claude Code CLI
9-
install_claude_code() {
10-
echo "Installing Claude Code CLI globally..."
7+
# Function to install pixi if not found
8+
install_pixi() {
9+
echo "Installing pixi..."
1110

12-
# Verify Node.js and npm are available (should be installed via dependsOn)
13-
if ! command -v node >/dev/null || ! command -v npm >/dev/null; then
14-
cat <<EOF
11+
# Detect architecture
12+
case "$(uname -m)" in
13+
x86_64|amd64) ARCH="x86_64" ;;
14+
aarch64|arm64) ARCH="aarch64" ;;
15+
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
16+
esac
1517

16-
ERROR: Node.js and npm are required but not found!
18+
# Download and install pixi
19+
curl -fsSL "https://github.com/prefix-dev/pixi/releases/latest/download/pixi-${ARCH}-unknown-linux-musl" -o /usr/local/bin/pixi
20+
chmod +x /usr/local/bin/pixi
1721

18-
This should not happen as the Node.js feature is declared in 'dependsOn'.
22+
echo "pixi installed successfully"
23+
pixi --version
24+
}
1925

20-
Please check:
21-
1. The devcontainer feature specification is correct
22-
2. The Node.js feature (ghcr.io/devcontainers/features/node) is available
23-
3. Your devcontainer build logs for errors
26+
# Function to install Claude Code CLI via pixi
27+
install_claude_code() {
28+
echo "Installing Claude Code CLI via pixi..."
2429

25-
EOF
26-
exit 1
30+
# Install pixi if not available
31+
if ! command -v pixi >/dev/null; then
32+
install_pixi
2733
fi
2834

29-
# Install with npm
30-
npm install -g @anthropic-ai/claude-code
35+
# Determine target user for pixi global install
36+
local target_user="${_REMOTE_USER:-vscode}"
37+
local target_home="${_REMOTE_USER_HOME:-/home/${target_user}}"
38+
39+
# Install with pixi global from blooop channel
40+
# Run as target user so it installs to their home directory
41+
if [ "$(id -u)" -eq 0 ] && [ "$target_user" != "root" ]; then
42+
su - "$target_user" -c "pixi global install --channel https://prefix.dev/blooop claude-shim"
43+
else
44+
pixi global install --channel https://prefix.dev/blooop claude-shim
45+
fi
3146

32-
# Verify installation
33-
if command -v claude >/dev/null; then
47+
# Add pixi paths to user's profile if not already there
48+
local profile="$target_home/.profile"
49+
local pixi_path_line='export PATH="$HOME/.pixi/envs/claude-shim/bin:$HOME/.pixi/bin:$PATH"'
50+
if [ -f "$profile" ] && ! grep -q "\.pixi/envs/claude-shim/bin" "$profile"; then
51+
echo "$pixi_path_line" >> "$profile"
52+
elif [ ! -f "$profile" ]; then
53+
echo "$pixi_path_line" > "$profile"
54+
chown "$target_user:$target_user" "$profile" 2>/dev/null || true
55+
fi
56+
57+
# Verify installation by checking the binary exists
58+
local pixi_bin_path="$target_home/.pixi/bin"
59+
local claude_bin="$pixi_bin_path/claude"
60+
if [ -x "$claude_bin" ]; then
3461
echo "Claude Code CLI installed successfully!"
35-
claude --version
62+
"$claude_bin" --version
3663
return 0
3764
else
38-
echo "ERROR: Claude Code CLI installation failed!"
65+
echo "ERROR: Claude Code CLI installation failed! Binary not found at $claude_bin"
3966
return 1
4067
fi
4168
}
4269

4370
# Function to create Claude configuration directories
44-
# These directories will be mounted from the host, but we create them
45-
# in the container to ensure they exist and have proper permissions
4671
create_claude_directories() {
4772
echo "Creating Claude configuration directories..."
4873

4974
# Determine the target user's home directory
50-
# $_REMOTE_USER is set by devcontainer, fallback to 'vscode'
5175
local target_user="${_REMOTE_USER:-vscode}"
5276
local target_home="${_REMOTE_USER_HOME:-/home/${target_user}}"
5377

54-
# Be defensive: if the resolved home does not exist, fall back to $HOME,
55-
# then to /home/${target_user}. If neither is available, fail clearly.
78+
# Be defensive: if the resolved home does not exist, fall back
5679
if [ ! -d "$target_home" ]; then
5780
if [ -n "${HOME:-}" ] && [ -d "$HOME" ]; then
5881
echo "Warning: target_home '$target_home' does not exist, falling back to \$HOME: $HOME" >&2
@@ -61,26 +84,21 @@ create_claude_directories() {
6184
echo "Warning: target_home '$target_home' does not exist, falling back to /home/${target_user}" >&2
6285
target_home="/home/${target_user}"
6386
else
64-
echo "Error: No suitable home directory found for '${target_user}'. Tried:" >&2
65-
echo " - _REMOTE_USER_HOME='${_REMOTE_USER_HOME:-}'" >&2
66-
echo " - \$HOME='${HOME:-}'" >&2
67-
echo " - /home/${target_user}" >&2
68-
echo "Please set _REMOTE_USER_HOME to a valid, writable directory." >&2
87+
echo "Error: No suitable home directory found for '${target_user}'." >&2
6988
exit 1
7089
fi
7190
fi
7291

7392
echo "Target home directory: $target_home"
7493
echo "Target user: $target_user"
7594

76-
# Create the main .claude directory
95+
# Create the main .claude directory and subdirectories
7796
mkdir -p "$target_home/.claude"
7897
mkdir -p "$target_home/.claude/agents"
7998
mkdir -p "$target_home/.claude/commands"
8099
mkdir -p "$target_home/.claude/hooks"
81100

82101
# Create empty config files if they don't exist
83-
# This ensures the bind mounts won't fail if files are missing on host
84102
if [ ! -f "$target_home/.claude/.credentials.json" ]; then
85103
echo "{}" > "$target_home/.claude/.credentials.json"
86104
chmod 600 "$target_home/.claude/.credentials.json"
@@ -92,26 +110,28 @@ create_claude_directories() {
92110
fi
93111

94112
# Set proper ownership
95-
# Note: These will be overridden by bind mounts from the host,
96-
# but this ensures the directories exist with correct permissions
97-
# if the mounts fail or for non-mounted directories
98113
if [ "$(id -u)" -eq 0 ]; then
99114
chown -R "$target_user:$target_user" "$target_home/.claude" || true
100115
fi
101116

102117
echo "Claude directories created successfully"
103118
}
104119

105-
# Main script starts here
120+
# Main script
106121
main() {
107122
echo "========================================="
108123
echo "Activating feature 'claude-code' (local)"
109124
echo "========================================="
110125

111-
# Install Claude Code CLI (or verify it's already installed)
112-
if command -v claude >/dev/null; then
126+
# Determine target paths
127+
local target_user="${_REMOTE_USER:-vscode}"
128+
local target_home="${_REMOTE_USER_HOME:-/home/${target_user}}"
129+
local claude_bin="$target_home/.pixi/bin/claude"
130+
131+
# Install Claude Code CLI
132+
if [ -x "$claude_bin" ]; then
113133
echo "Claude Code CLI is already installed"
114-
claude --version
134+
"$claude_bin" --version
115135
else
116136
install_claude_code || exit 1
117137
fi
@@ -123,27 +143,11 @@ main() {
123143
echo "Claude Code feature activated successfully!"
124144
echo "========================================="
125145
echo ""
126-
echo "Configuration files mounted from host:"
127-
echo " Read-Write (auth & state):"
128-
echo " - ~/.claude/.credentials.json (OAuth tokens)"
129-
echo " - ~/.claude/.claude.json (account, setup tracking)"
130-
echo ""
131-
echo " Read-Only (security-protected):"
132-
echo " - ~/.claude/CLAUDE.md"
133-
echo " - ~/.claude/settings.json"
134-
echo " - ~/.claude/agents/"
135-
echo " - ~/.claude/commands/"
136-
echo " - ~/.claude/hooks/"
137-
echo ""
138-
echo "Authentication:"
139-
echo " - If you're already authenticated on your host, credentials are shared"
140-
echo " - Otherwise, run 'claude' and follow the OAuth flow"
141-
echo " - The OAuth callback may open in your host browser"
142-
echo " - Credentials are stored on your host at ~/.claude/.credentials.json"
146+
echo "Configuration is stored in a Docker volume (claude-config)"
147+
echo "and persists between container rebuilds."
143148
echo ""
144-
echo "To modify config files, edit on your host machine and rebuild the container."
149+
echo "To authenticate, run 'claude' and follow the OAuth flow."
145150
echo ""
146151
}
147152

148-
# Execute main function
149153
main

.devcontainer/devcontainer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"dockerfile": "Dockerfile",
55
"context": ".."
66
},
7+
"initializeCommand": ".devcontainer/claude-code/init-host.sh",
78
"customizations": {
89
"vscode": {
910
"settings": {},

0 commit comments

Comments
 (0)