Skip to content

Commit 7188153

Browse files
authored
Merge pull request #116 from blooop/fix/portable-devcontainer-claude
fix: make devcontainer portable across hosts
2 parents a989caf + 2cd30e7 commit 7188153

File tree

5 files changed

+135
-101
lines changed

5 files changed

+135
-101
lines changed

.devcontainer/Dockerfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ 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/bin:$PATH"' >> /home/vscode/.profile \
15+
&& echo '# Workaround: pixi trampoline fails for bash scripts, so add env bin directly' >> /home/vscode/.profile \
16+
&& echo '[ -d "$HOME/.pixi/envs/claude-shim/bin" ] && export PATH="$HOME/.pixi/envs/claude-shim/bin:$PATH"' >> /home/vscode/.profile
1417

1518
# Create .ssh directory with proper permissions for SSH config mounts
1619
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: 121 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -2,116 +2,165 @@
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
6+
7+
# Global variables set by resolve_target_home
8+
TARGET_USER=""
9+
TARGET_HOME=""
10+
11+
# Function to resolve target user and home directory with validation
12+
# Sets TARGET_USER and TARGET_HOME global variables
13+
resolve_target_home() {
14+
TARGET_USER="${_REMOTE_USER:-vscode}"
15+
TARGET_HOME="${_REMOTE_USER_HOME:-}"
16+
17+
# If _REMOTE_USER_HOME is not set, try to infer from current user or /home/<user>
18+
if [ -z "${TARGET_HOME}" ]; then
19+
if [ "$(id -un 2>/dev/null)" = "${TARGET_USER}" ] && [ -n "${HOME:-}" ]; then
20+
TARGET_HOME="${HOME}"
21+
elif [ -d "/home/${TARGET_USER}" ]; then
22+
TARGET_HOME="/home/${TARGET_USER}"
23+
fi
24+
fi
725

8-
# Function to install Claude Code CLI
9-
install_claude_code() {
10-
echo "Installing Claude Code CLI globally..."
26+
# If TARGET_HOME is set but doesn't exist, try fallbacks
27+
if [ -n "${TARGET_HOME}" ] && [ ! -d "${TARGET_HOME}" ]; then
28+
if [ -n "${HOME:-}" ] && [ -d "$HOME" ]; then
29+
echo "Warning: TARGET_HOME '${TARGET_HOME}' does not exist, falling back to \$HOME: $HOME" >&2
30+
TARGET_HOME="$HOME"
31+
elif [ -d "/home/${TARGET_USER}" ]; then
32+
echo "Warning: TARGET_HOME '${TARGET_HOME}' does not exist, falling back to /home/${TARGET_USER}" >&2
33+
TARGET_HOME="/home/${TARGET_USER}"
34+
fi
35+
fi
1136

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
37+
# Ensure we ended up with a valid, existing home directory
38+
if [ -z "${TARGET_HOME}" ] || [ ! -d "${TARGET_HOME}" ]; then
39+
echo "Error: could not determine a valid home directory for user '${TARGET_USER}'." >&2
40+
echo "Checked _REMOTE_USER_HOME ('${_REMOTE_USER_HOME:-}'), \$HOME ('${HOME:-}'), and /home/${TARGET_USER}." >&2
41+
exit 1
42+
fi
43+
}
1544

16-
ERROR: Node.js and npm are required but not found!
45+
# Function to install pixi if not found
46+
install_pixi() {
47+
echo "Installing pixi..."
1748

18-
This should not happen as the Node.js feature is declared in 'dependsOn'.
49+
# Detect architecture
50+
case "$(uname -m)" in
51+
x86_64|amd64) ARCH="x86_64" ;;
52+
aarch64|arm64) ARCH="aarch64" ;;
53+
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
54+
esac
1955

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
56+
# Download and install pixi
57+
curl -fsSL "https://github.com/prefix-dev/pixi/releases/latest/download/pixi-${ARCH}-unknown-linux-musl" -o /usr/local/bin/pixi
58+
chmod +x /usr/local/bin/pixi
2459

25-
EOF
26-
exit 1
60+
echo "pixi installed successfully"
61+
pixi --version
62+
}
63+
64+
# Function to install Claude Code CLI via pixi
65+
install_claude_code() {
66+
echo "Installing Claude Code CLI via pixi..."
67+
68+
# Install pixi if not available
69+
if ! command -v pixi >/dev/null; then
70+
install_pixi
2771
fi
2872

29-
# Install with npm
30-
npm install -g @anthropic-ai/claude-code
73+
# Resolve target user and home (sets TARGET_USER and TARGET_HOME)
74+
resolve_target_home
3175

32-
# Verify installation
33-
if command -v claude >/dev/null; then
76+
# Install with pixi global from blooop channel
77+
# Run as target user so it installs to their home directory
78+
if [ "$(id -u)" -eq 0 ] && [ "$TARGET_USER" != "root" ]; then
79+
su - "$TARGET_USER" -c "pixi global install --channel https://prefix.dev/blooop claude-shim"
80+
else
81+
pixi global install --channel https://prefix.dev/blooop claude-shim
82+
fi
83+
84+
# Add pixi bin path to user's profile if not already there
85+
local profile="$TARGET_HOME/.profile"
86+
local pixi_path_line='export PATH="$HOME/.pixi/bin:$PATH"'
87+
if [ -f "$profile" ] && ! grep -q '\.pixi/bin' "$profile"; then
88+
echo "$pixi_path_line" >> "$profile"
89+
elif [ ! -f "$profile" ]; then
90+
echo "$pixi_path_line" > "$profile"
91+
chown "$TARGET_USER:$TARGET_USER" "$profile" 2>/dev/null || true
92+
fi
93+
94+
# Workaround: pixi trampoline fails for bash scripts, so add env bin directly
95+
# This conditionally adds the path only if the env exists
96+
local env_path_line='[ -d "$HOME/.pixi/envs/claude-shim/bin" ] && export PATH="$HOME/.pixi/envs/claude-shim/bin:$PATH"'
97+
if [ -f "$profile" ] && ! grep -q 'pixi/envs/claude-shim' "$profile"; then
98+
echo "# Workaround: pixi trampoline fails for bash scripts" >> "$profile"
99+
echo "$env_path_line" >> "$profile"
100+
fi
101+
102+
# Verify installation by checking the trampoline exists (don't run it - that triggers download)
103+
local pixi_bin_path="$TARGET_HOME/.pixi/bin"
104+
local claude_bin="$pixi_bin_path/claude"
105+
if [ -x "$claude_bin" ]; then
34106
echo "Claude Code CLI installed successfully!"
35-
claude --version
107+
echo "(Claude binary will be downloaded on first run)"
36108
return 0
37109
else
38-
echo "ERROR: Claude Code CLI installation failed!"
110+
echo "ERROR: Claude Code CLI installation failed! Binary not found at $claude_bin"
39111
return 1
40112
fi
41113
}
42114

43115
# 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
46116
create_claude_directories() {
47117
echo "Creating Claude configuration directories..."
48118

49-
# Determine the target user's home directory
50-
# $_REMOTE_USER is set by devcontainer, fallback to 'vscode'
51-
local target_user="${_REMOTE_USER:-vscode}"
52-
local target_home="${_REMOTE_USER_HOME:-/home/${target_user}}"
53-
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.
56-
if [ ! -d "$target_home" ]; then
57-
if [ -n "${HOME:-}" ] && [ -d "$HOME" ]; then
58-
echo "Warning: target_home '$target_home' does not exist, falling back to \$HOME: $HOME" >&2
59-
target_home="$HOME"
60-
elif [ -d "/home/${target_user}" ]; then
61-
echo "Warning: target_home '$target_home' does not exist, falling back to /home/${target_user}" >&2
62-
target_home="/home/${target_user}"
63-
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
69-
exit 1
70-
fi
71-
fi
119+
# Resolve target user and home (sets TARGET_USER and TARGET_HOME)
120+
resolve_target_home
72121

73-
echo "Target home directory: $target_home"
74-
echo "Target user: $target_user"
122+
echo "Target home directory: $TARGET_HOME"
123+
echo "Target user: $TARGET_USER"
75124

76-
# Create the main .claude directory
77-
mkdir -p "$target_home/.claude"
78-
mkdir -p "$target_home/.claude/agents"
79-
mkdir -p "$target_home/.claude/commands"
80-
mkdir -p "$target_home/.claude/hooks"
125+
# Create the main .claude directory and subdirectories
126+
mkdir -p "$TARGET_HOME/.claude"
127+
mkdir -p "$TARGET_HOME/.claude/agents"
128+
mkdir -p "$TARGET_HOME/.claude/commands"
129+
mkdir -p "$TARGET_HOME/.claude/hooks"
81130

82131
# Create empty config files if they don't exist
83-
# This ensures the bind mounts won't fail if files are missing on host
84-
if [ ! -f "$target_home/.claude/.credentials.json" ]; then
85-
echo "{}" > "$target_home/.claude/.credentials.json"
86-
chmod 600 "$target_home/.claude/.credentials.json"
132+
if [ ! -f "$TARGET_HOME/.claude/.credentials.json" ]; then
133+
echo "{}" > "$TARGET_HOME/.claude/.credentials.json"
134+
chmod 600 "$TARGET_HOME/.claude/.credentials.json"
87135
fi
88136

89-
if [ ! -f "$target_home/.claude/.claude.json" ]; then
90-
echo "{}" > "$target_home/.claude/.claude.json"
91-
chmod 600 "$target_home/.claude/.claude.json"
137+
if [ ! -f "$TARGET_HOME/.claude/.claude.json" ]; then
138+
echo "{}" > "$TARGET_HOME/.claude/.claude.json"
139+
chmod 600 "$TARGET_HOME/.claude/.claude.json"
92140
fi
93141

94142
# 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
98143
if [ "$(id -u)" -eq 0 ]; then
99-
chown -R "$target_user:$target_user" "$target_home/.claude" || true
144+
chown -R "$TARGET_USER:$TARGET_USER" "$TARGET_HOME/.claude" || true
100145
fi
101146

102147
echo "Claude directories created successfully"
103148
}
104149

105-
# Main script starts here
150+
# Main script
106151
main() {
107152
echo "========================================="
108153
echo "Activating feature 'claude-code' (local)"
109154
echo "========================================="
110155

111-
# Install Claude Code CLI (or verify it's already installed)
112-
if command -v claude >/dev/null; then
156+
# Resolve target user and home (sets TARGET_USER and TARGET_HOME)
157+
resolve_target_home
158+
159+
local claude_bin="$TARGET_HOME/.pixi/bin/claude"
160+
161+
# Install Claude Code CLI
162+
if [ -x "$claude_bin" ]; then
113163
echo "Claude Code CLI is already installed"
114-
claude --version
115164
else
116165
install_claude_code || exit 1
117166
fi
@@ -123,27 +172,11 @@ main() {
123172
echo "Claude Code feature activated successfully!"
124173
echo "========================================="
125174
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"
175+
echo "Configuration is bind-mounted from the host (~/.claude)"
176+
echo "and persists across container rebuilds."
143177
echo ""
144-
echo "To modify config files, edit on your host machine and rebuild the container."
178+
echo "To authenticate, run 'claude' and follow the OAuth flow."
145179
echo ""
146180
}
147181

148-
# Execute main function
149182
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)