Skip to content

Commit 4bf74f2

Browse files
chapterjasonclaude
andcommitted
Drop PATH symlinks in favour of profile hooks; run claude-code as user
scripts/nvm/install.sh: - Remove the /usr/local/bin/{node,npm,npx,corepack} symlinks. - /etc/profile.d/nvm.sh now calls `nvm use default` after sourcing nvm, so node/npm land on PATH via the default alias on shell source. scripts/web-shell/install.sh: - Drop the /usr/local/bin/web-shell symlink. - Systemd unit's ExecStart and the non-systemd login-shell fallback use the absolute $WS_BIN ($NPM_PREFIX/bin/web-shell) resolved at install. scripts/claude-code/install.sh: - Runs as the remote user instead of root+HOME+chown (see Dockerfile). - Appends a PATH hook to ~/.profile that runtime-checks whether $HOME/.local/bin is already in $PATH and only prepends if not. - /etc/home-persist.d/claude-code.json written via sudo tee. src/base/Dockerfile: - Split the install loop: nvm / rtk / context-mode / web-shell / home-persist as root, claude-code as the remote user (upstream installer writes land in $HOME with correct ownership). main.tf: - coder_script.lifecycle_init sources ~/.profile at the top so PATH hooks apply before context-mode / rtk post-create hooks run (they look up `claude` on PATH). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c56cc1d commit 4bf74f2

5 files changed

Lines changed: 37 additions & 40 deletions

File tree

main.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,10 @@ resource "coder_script" "lifecycle_init" {
308308
script = <<-EOT
309309
set -e
310310
311+
# Source ~/.profile so PATH hooks (e.g. $HOME/.local/bin from claude-code)
312+
# take effect for the post-create scripts invoked below.
313+
[ -f "$HOME/.profile" ] && . "$HOME/.profile"
314+
311315
user_paths="${data.coder_parameter.home_persist_paths.value}"
312316
if [ -n "$user_paths" ]; then
313317
paths_json='[]'

scripts/claude-code/install.sh

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,28 @@
11
#!/usr/bin/env bash
2-
# claude-code installer.
2+
# claude-code installer. Runs as the workspace user (see src/base/Dockerfile).
33
set -e
44

5-
USER_NAME="${_REMOTE_USER:-${USERNAME:-root}}"
6-
if [ "$USER_NAME" = "root" ]; then
7-
USER_GROUP="root"
8-
else
9-
USER_GROUP="$(id -gn "$USER_NAME")"
10-
fi
11-
12-
USER_HOME="$(getent passwd "$USER_NAME" | cut -d: -f6)"
13-
14-
HOME="$USER_HOME" curl -fsSL https://claude.ai/install.sh | HOME="$USER_HOME" bash
5+
curl -fsSL https://claude.ai/install.sh | bash
156

16-
if [ ! -x "$USER_HOME/.local/bin/claude" ]; then
17-
echo "claude-code: expected $USER_HOME/.local/bin/claude after install, but it was not found." >&2
7+
if [ ! -x "$HOME/.local/bin/claude" ]; then
8+
echo "claude-code: expected $HOME/.local/bin/claude after install, but it was not found." >&2
189
exit 1
1910
fi
2011

21-
# The upstream installer runs as root with HOME=$USER_HOME and writes into
22-
# $HOME/.claude, $HOME/.local AND $HOME/.cache (its own state dir plus
23-
# node-gyp from any native-module compile). Chown all three so nothing is
24-
# left root-owned on the remote user's home. .cache is guarded because it
25-
# may not exist on minimal installs.
26-
chown -R "$USER_NAME:$USER_GROUP" "$USER_HOME/.claude" "$USER_HOME/.local"
27-
if [ -d "$USER_HOME/.cache" ]; then
28-
chown -R "$USER_NAME:$USER_GROUP" "$USER_HOME/.cache"
12+
# PATH hook in ~/.profile — if-check at source time only prepends
13+
# $HOME/.local/bin when it's not already in $PATH.
14+
cat >> "$HOME/.profile" <<EOF
15+
16+
if ! echo ":\$PATH:" | grep -q ":\$HOME/.local/bin:"; then
17+
export PATH="\$HOME/.local/bin:\$PATH"
2918
fi
19+
EOF
3020

3121
# Declare the HOME paths Claude Code needs persisted. The home-persist
3222
# resolver reads every /etc/home-persist.d/*.json at workspace start and
33-
# symlinks these into /mnt/home-persist.
34-
mkdir -p /etc/home-persist.d
35-
cat > /etc/home-persist.d/claude-code.json <<'EOF'
23+
# symlinks these into /mnt/home-persist. /etc/home-persist.d is root-owned.
24+
sudo mkdir -p /etc/home-persist.d
25+
sudo tee /etc/home-persist.d/claude-code.json >/dev/null <<'EOF'
3626
{
3727
"source": "claude-code",
3828
"paths": [".claude/", ".claude.json"]

scripts/nvm/install.sh

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install
3030

3131
cat >/etc/profile.d/nvm.sh <<EOF
3232
export NVM_DIR="$NVM_DIR"
33-
[ -s "\$NVM_DIR/nvm.sh" ] && \\. "\$NVM_DIR/nvm.sh"
34-
[ -s "\$NVM_DIR/bash_completion" ] && \\. "\$NVM_DIR/bash_completion"
33+
if [ -s "\$NVM_DIR/nvm.sh" ]; then
34+
\\. "\$NVM_DIR/nvm.sh"
35+
[ -s "\$NVM_DIR/bash_completion" ] && \\. "\$NVM_DIR/bash_completion"
36+
# Activate the default alias if present — puts node/npm/npx on PATH.
37+
[ -s "\$NVM_DIR/alias/default" ] && nvm use default >/dev/null 2>&1
38+
fi
3539
EOF
3640
chmod 644 /etc/profile.d/nvm.sh
3741

@@ -75,12 +79,6 @@ if [ "$NODE_VERSION" != "none" ]; then
7579
# shellcheck disable=SC2086
7680
nvm install $NVM_INSTALL_ARG
7781
nvm alias default "$NVM_ALIAS_TARGET"
78-
79-
NODE_BIN_DIR="$(dirname "$(nvm which default)")"
80-
for bin in node npm npx corepack; do
81-
[ -x "$NODE_BIN_DIR/$bin" ] || continue
82-
ln -sf "$NODE_BIN_DIR/$bin" "/usr/local/bin/$bin"
83-
done
8482
fi
8583

8684
chown -R "$USER_NAME:$USER_GROUP" "$NVM_DIR"

scripts/web-shell/install.sh

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,14 @@ curl -fsSL -o "$TMPDIR/web-shell.tgz" "$TARBALL_URL"
8282

8383
npm install -g "$TMPDIR/web-shell.tgz"
8484

85-
# 5. Stable symlink at /usr/local/bin/web-shell — the nvm prefix isn't on the
86-
# systemd service PATH.
85+
# 5. Resolve the absolute path to the binary so the systemd unit doesn't
86+
# depend on $PATH / symlinks at service start time.
8787
NPM_PREFIX="$(npm config get prefix)"
8888
WS_BIN="$NPM_PREFIX/bin/web-shell"
8989
if [ ! -x "$WS_BIN" ]; then
9090
echo "web-shell: $WS_BIN missing after npm install." >&2
9191
exit 1
9292
fi
93-
if [ "$WS_BIN" != "/usr/local/bin/web-shell" ]; then
94-
ln -sf "$WS_BIN" /usr/local/bin/web-shell
95-
fi
9693

9794
# 6. Systemd unit. We always write it — even when systemd isn't PID 1 right
9895
# now, a later rebase onto a systemd base won't need to reinstall.
@@ -110,7 +107,7 @@ WorkingDirectory=${USER_HOME}
110107
Environment=HOME=${USER_HOME}
111108
Environment=HOST=127.0.0.1
112109
Environment=PORT=${WS_PORT}
113-
ExecStart=/usr/local/bin/web-shell
110+
ExecStart=${WS_BIN}
114111
Restart=on-failure
115112
RestartSec=1
116113
@@ -142,7 +139,7 @@ else
142139
# supervisor is used instead.
143140
if ! pgrep -u "\$(id -u)" -f '/usr/local/bin/web-shell' >/dev/null 2>&1; then
144141
HOST='127.0.0.1' PORT='${WS_PORT}' \\
145-
nohup sh -c 'while true; do /usr/local/bin/web-shell; sleep 1; done' \\
142+
nohup sh -c 'while true; do ${WS_BIN}; sleep 1; done' \\
146143
> /tmp/web-shell.log 2>&1 &
147144
disown >/dev/null 2>&1 || true
148145
fi

src/base/Dockerfile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,19 @@ USER root
7777
# env var the devcontainer CLI used at feature-install time.
7878
ENV _REMOTE_USER=${USERNAME}
7979

80+
# System-wide installers run as root.
8081
RUN --mount=type=bind,source=scripts,target=/scripts \
81-
for s in nvm claude-code rtk context-mode web-shell home-persist; do \
82+
for s in nvm rtk context-mode web-shell home-persist; do \
8283
bash "/scripts/$s/install.sh"; \
8384
done
8485

86+
# Per-user installers run as the remote user directly — upstream installer
87+
# writes land in $HOME with correct ownership; no root+chown dance.
88+
USER ${USERNAME}
89+
RUN --mount=type=bind,source=scripts,target=/scripts \
90+
bash "/scripts/claude-code/install.sh"
91+
USER root
92+
8593
# Coder agent: systemd unit runs /etc/coder/agent-init.sh as the workspace
8694
# user after dockerd is up. The script itself is uploaded at container-create
8795
# time by the Terraform template (kreuzwerker/docker `upload` block).

0 commit comments

Comments
 (0)