22set -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
46116create_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
106151main () {
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
149182main
0 commit comments