Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ RUN curl -L -o /usr/local/bin/pixi -fsSL --compressed "https://github.com/prefix
USER vscode
WORKDIR /home/vscode

RUN echo 'eval "$(pixi completion -s bash)"' >> /home/vscode/.bashrc
RUN echo 'eval "$(pixi completion -s bash)"' >> /home/vscode/.bashrc \
&& echo 'export PATH="$HOME/.pixi/bin:$PATH"' >> /home/vscode/.profile \
&& echo '# Workaround: pixi trampoline fails for bash scripts, so add env bin directly' >> /home/vscode/.profile \
&& echo '[ -d "$HOME/.pixi/envs/claude-shim/bin" ] && export PATH="$HOME/.pixi/envs/claude-shim/bin:$PATH"' >> /home/vscode/.profile

# Create .ssh directory with proper permissions for SSH config mounts
RUN mkdir -p /home/vscode/.ssh && chmod 700 /home/vscode/.ssh
15 changes: 3 additions & 12 deletions .devcontainer/claude-code/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "Claude Code CLI",
"id": "claude-code",
"version": "0.1.0",
"description": "Installs Claude Code CLI globally and mounts configuration directories",
"version": "0.2.0",
"description": "Installs Claude Code CLI via pixi with persistent configuration",
"options": {},
"documentationURL": "https://github.com/anthropics/devcontainer-features",
"licenseURL": "https://github.com/anthropics/devcontainer-features/blob/main/LICENSE",
Expand All @@ -16,16 +16,7 @@
"containerEnv": {
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude"
},
"dependsOn": {
"ghcr.io/devcontainers/features/node": {}
},
"mounts": [
"source=${localEnv:HOME}/.claude/CLAUDE.md,target=/home/vscode/.claude/CLAUDE.md,type=bind,ro",
"source=${localEnv:HOME}/.claude/settings.json,target=/home/vscode/.claude/settings.json,type=bind,ro",
"source=${localEnv:HOME}/.claude/.credentials.json,target=/home/vscode/.claude/.credentials.json,type=bind",
"source=${localEnv:HOME}/.claude/.claude.json,target=/home/vscode/.claude/.claude.json,type=bind",
"source=${localEnv:HOME}/.claude/agents,target=/home/vscode/.claude/agents,type=bind,ro",
"source=${localEnv:HOME}/.claude/commands,target=/home/vscode/.claude/commands,type=bind,ro",
"source=${localEnv:HOME}/.claude/hooks,target=/home/vscode/.claude/hooks,type=bind,ro"
"source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind"
]
}
6 changes: 6 additions & 0 deletions .devcontainer/claude-code/init-host.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh
# Initialize Claude Code host directory for devcontainer bind mount
# This script runs on the HOST before the container is created.
# mkdir -p is idempotent - it only creates if missing, won't clobber existing.

mkdir -p "$HOME/.claude"
209 changes: 121 additions & 88 deletions .devcontainer/claude-code/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,116 +2,165 @@
set -eu

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

# Global variables set by resolve_target_home
TARGET_USER=""
TARGET_HOME=""

# Function to resolve target user and home directory with validation
# Sets TARGET_USER and TARGET_HOME global variables
resolve_target_home() {
TARGET_USER="${_REMOTE_USER:-vscode}"
TARGET_HOME="${_REMOTE_USER_HOME:-}"

# If _REMOTE_USER_HOME is not set, try to infer from current user or /home/<user>
if [ -z "${TARGET_HOME}" ]; then
if [ "$(id -un 2>/dev/null)" = "${TARGET_USER}" ] && [ -n "${HOME:-}" ]; then
TARGET_HOME="${HOME}"
elif [ -d "/home/${TARGET_USER}" ]; then
TARGET_HOME="/home/${TARGET_USER}"
fi
fi

# Function to install Claude Code CLI
install_claude_code() {
echo "Installing Claude Code CLI globally..."
# If TARGET_HOME is set but doesn't exist, try fallbacks
if [ -n "${TARGET_HOME}" ] && [ ! -d "${TARGET_HOME}" ]; then
if [ -n "${HOME:-}" ] && [ -d "$HOME" ]; then
echo "Warning: TARGET_HOME '${TARGET_HOME}' does not exist, falling back to \$HOME: $HOME" >&2
TARGET_HOME="$HOME"
elif [ -d "/home/${TARGET_USER}" ]; then
echo "Warning: TARGET_HOME '${TARGET_HOME}' does not exist, falling back to /home/${TARGET_USER}" >&2
TARGET_HOME="/home/${TARGET_USER}"
fi
fi

# Verify Node.js and npm are available (should be installed via dependsOn)
if ! command -v node >/dev/null || ! command -v npm >/dev/null; then
cat <<EOF
# Ensure we ended up with a valid, existing home directory
if [ -z "${TARGET_HOME}" ] || [ ! -d "${TARGET_HOME}" ]; then
echo "Error: could not determine a valid home directory for user '${TARGET_USER}'." >&2
echo "Checked _REMOTE_USER_HOME ('${_REMOTE_USER_HOME:-}'), \$HOME ('${HOME:-}'), and /home/${TARGET_USER}." >&2
exit 1
fi
}

ERROR: Node.js and npm are required but not found!
# Function to install pixi if not found
install_pixi() {
echo "Installing pixi..."

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

Please check:
1. The devcontainer feature specification is correct
2. The Node.js feature (ghcr.io/devcontainers/features/node) is available
3. Your devcontainer build logs for errors
# Download and install pixi
curl -fsSL "https://github.com/prefix-dev/pixi/releases/latest/download/pixi-${ARCH}-unknown-linux-musl" -o /usr/local/bin/pixi
chmod +x /usr/local/bin/pixi

EOF
exit 1
echo "pixi installed successfully"
pixi --version
}

# Function to install Claude Code CLI via pixi
install_claude_code() {
echo "Installing Claude Code CLI via pixi..."

# Install pixi if not available
if ! command -v pixi >/dev/null; then
install_pixi
fi

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

# Verify installation
if command -v claude >/dev/null; then
# Install with pixi global from blooop channel
# Run as target user so it installs to their home directory
if [ "$(id -u)" -eq 0 ] && [ "$TARGET_USER" != "root" ]; then
su - "$TARGET_USER" -c "pixi global install --channel https://prefix.dev/blooop claude-shim"
else
pixi global install --channel https://prefix.dev/blooop claude-shim
fi

# Add pixi bin path to user's profile if not already there
local profile="$TARGET_HOME/.profile"
local pixi_path_line='export PATH="$HOME/.pixi/bin:$PATH"'
if [ -f "$profile" ] && ! grep -q '\.pixi/bin' "$profile"; then
echo "$pixi_path_line" >> "$profile"
elif [ ! -f "$profile" ]; then
echo "$pixi_path_line" > "$profile"
chown "$TARGET_USER:$TARGET_USER" "$profile" 2>/dev/null || true
fi

# Workaround: pixi trampoline fails for bash scripts, so add env bin directly
# This conditionally adds the path only if the env exists
local env_path_line='[ -d "$HOME/.pixi/envs/claude-shim/bin" ] && export PATH="$HOME/.pixi/envs/claude-shim/bin:$PATH"'
if [ -f "$profile" ] && ! grep -q 'pixi/envs/claude-shim' "$profile"; then
echo "# Workaround: pixi trampoline fails for bash scripts" >> "$profile"
echo "$env_path_line" >> "$profile"
fi

# Verify installation by checking the trampoline exists (don't run it - that triggers download)
local pixi_bin_path="$TARGET_HOME/.pixi/bin"
local claude_bin="$pixi_bin_path/claude"
if [ -x "$claude_bin" ]; then
echo "Claude Code CLI installed successfully!"
claude --version
echo "(Claude binary will be downloaded on first run)"
return 0
else
echo "ERROR: Claude Code CLI installation failed!"
echo "ERROR: Claude Code CLI installation failed! Binary not found at $claude_bin"
return 1
fi
}

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

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

# Be defensive: if the resolved home does not exist, fall back to $HOME,
# then to /home/${target_user}. If neither is available, fail clearly.
if [ ! -d "$target_home" ]; then
if [ -n "${HOME:-}" ] && [ -d "$HOME" ]; then
echo "Warning: target_home '$target_home' does not exist, falling back to \$HOME: $HOME" >&2
target_home="$HOME"
elif [ -d "/home/${target_user}" ]; then
echo "Warning: target_home '$target_home' does not exist, falling back to /home/${target_user}" >&2
target_home="/home/${target_user}"
else
echo "Error: No suitable home directory found for '${target_user}'. Tried:" >&2
echo " - _REMOTE_USER_HOME='${_REMOTE_USER_HOME:-}'" >&2
echo " - \$HOME='${HOME:-}'" >&2
echo " - /home/${target_user}" >&2
echo "Please set _REMOTE_USER_HOME to a valid, writable directory." >&2
exit 1
fi
fi
# Resolve target user and home (sets TARGET_USER and TARGET_HOME)
resolve_target_home

echo "Target home directory: $target_home"
echo "Target user: $target_user"
echo "Target home directory: $TARGET_HOME"
echo "Target user: $TARGET_USER"

# Create the main .claude directory
mkdir -p "$target_home/.claude"
mkdir -p "$target_home/.claude/agents"
mkdir -p "$target_home/.claude/commands"
mkdir -p "$target_home/.claude/hooks"
# Create the main .claude directory and subdirectories
mkdir -p "$TARGET_HOME/.claude"
mkdir -p "$TARGET_HOME/.claude/agents"
mkdir -p "$TARGET_HOME/.claude/commands"
mkdir -p "$TARGET_HOME/.claude/hooks"

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

if [ ! -f "$target_home/.claude/.claude.json" ]; then
echo "{}" > "$target_home/.claude/.claude.json"
chmod 600 "$target_home/.claude/.claude.json"
if [ ! -f "$TARGET_HOME/.claude/.claude.json" ]; then
echo "{}" > "$TARGET_HOME/.claude/.claude.json"
chmod 600 "$TARGET_HOME/.claude/.claude.json"
fi

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

echo "Claude directories created successfully"
}

# Main script starts here
# Main script
main() {
echo "========================================="
echo "Activating feature 'claude-code' (local)"
echo "========================================="

# Install Claude Code CLI (or verify it's already installed)
if command -v claude >/dev/null; then
# Resolve target user and home (sets TARGET_USER and TARGET_HOME)
resolve_target_home

local claude_bin="$TARGET_HOME/.pixi/bin/claude"

# Install Claude Code CLI
if [ -x "$claude_bin" ]; then
echo "Claude Code CLI is already installed"
claude --version
else
install_claude_code || exit 1
fi
Expand All @@ -123,27 +172,11 @@ main() {
echo "Claude Code feature activated successfully!"
echo "========================================="
echo ""
echo "Configuration files mounted from host:"
echo " Read-Write (auth & state):"
echo " - ~/.claude/.credentials.json (OAuth tokens)"
echo " - ~/.claude/.claude.json (account, setup tracking)"
echo ""
echo " Read-Only (security-protected):"
echo " - ~/.claude/CLAUDE.md"
echo " - ~/.claude/settings.json"
echo " - ~/.claude/agents/"
echo " - ~/.claude/commands/"
echo " - ~/.claude/hooks/"
echo ""
echo "Authentication:"
echo " - If you're already authenticated on your host, credentials are shared"
echo " - Otherwise, run 'claude' and follow the OAuth flow"
echo " - The OAuth callback may open in your host browser"
echo " - Credentials are stored on your host at ~/.claude/.credentials.json"
echo "Configuration is bind-mounted from the host (~/.claude)"
echo "and persists across container rebuilds."
echo ""
echo "To modify config files, edit on your host machine and rebuild the container."
echo "To authenticate, run 'claude' and follow the OAuth flow."
echo ""
}

# Execute main function
main
1 change: 1 addition & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"dockerfile": "Dockerfile",
"context": ".."
},
"initializeCommand": ".devcontainer/claude-code/init-host.sh",
"customizations": {
"vscode": {
"settings": {},
Expand Down