I've started using cloud environments in Claude Code, and they run on a pre-set Node version. It's been a bit of a pain to get one to use the project's .nvmrc file to set the environment's Node version correctly, so I figured it was worth sharing the snippet.
#!/bin/bash
# Cloud environment setup: nvm, with `node` following the repo's .nvmrc / .node-version (default 24).
set -euo pipefail
DEFAULT_NODE=24
export NVM_DIR=/opt/nvm
# 1. nvm. The installer also adds its loader to ~/.bashrc, so interactive shells get `nvm`.
if [ ! -s "$NVM_DIR/nvm.sh" ]; then
mkdir -p "$NVM_DIR"
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
fi
# Anything that assumes nvm's default location finds the same install.
[ -e ~/.nvm ] || ln -s "$NVM_DIR" ~/.nvm
# 2. use-repo-node [dir] point `node` at the version dir pins (latest $DEFAULT_NODE.x if none)
# use-repo-node --version <v> point `node` at <v> for the rest of the session
cat > /usr/local/bin/use-repo-node <<EOF
#!/bin/bash
set -euo pipefail
export NVM_DIR=$NVM_DIR
want=""
if [ "\${1:-}" = --version ]; then
[ -n "\${2:-}" ] || { echo "usage: use-repo-node [dir] | --version <version>" >&2; exit 2; }
repo="--version"; want="\${2#v}"
else
repo="\${1:-\$PWD}"
for f in .nvmrc .node-version; do
if [ -f "\$repo/\$f" ]; then
want=\$(grep -v '^[[:space:]]*#' "\$repo/\$f" | tr -d '[:space:]'); want="\${want#v}"; break
fi
done
fi
case "\$want" in ""|system) want=$DEFAULT_NODE ;; esac # lts/*, node etc. are resolved by nvm
set +eu; . "\$NVM_DIR/nvm.sh"; set -eu # nvm.sh exits non-zero and reads unset vars
nvm install "\$want" >/dev/null 2>&1 || { echo "use-repo-node: nvm could not install '\$want'" >&2; exit 1; }
nvm alias default "\$want" >/dev/null # so shells that source nvm.sh agree
ln -sfn "\$(dirname "\$(nvm which "\$want")")" /opt/node-default
if [ -n "\${CLAUDE_ENV_FILE:-}" ] && ! grep -qs '/opt/node-default:' "\$CLAUDE_ENV_FILE"; then
# Bash-tool shells never read ~/.bashrc: load nvm here (--no-use: define the command,
# leave PATH alone), then put the repo's version first.
cat >> "\$CLAUDE_ENV_FILE" <<'ENV'
export NVM_DIR=$NVM_DIR
[ -s "\$NVM_DIR/nvm.sh" ] && . "\$NVM_DIR/nvm.sh" --no-use || true
export PATH="/opt/node-default:\$PATH"
ENV
fi
echo "node \$(/opt/node-default/node --version) (from \${repo}: \$want)"
EOF
chmod +x /usr/local/bin/use-repo-node
# 3. Default version now, so /opt/node-default exists before the first session.
use-repo-node /nonexistent
# 4. At each Claude Code session start, once the repo is checked out: pick the repo's version,
# then put /opt/node-default ahead of the image's preinstalled Node dirs for every Bash
# command, via CLAUDE_ENV_FILE. Nothing preinstalled is overwritten. Merged into existing
# user settings rather than overwriting them.
SETTINGS=~/.claude/settings.json
mkdir -p ~/.claude
[ -s "$SETTINGS" ] || echo '{}' > "$SETTINGS"
jq '.hooks.SessionStart = ((.hooks.SessionStart // [])
| map(select((.hooks // []) | all(.command != "use-repo-node \"$CLAUDE_PROJECT_DIR\"")))
+ [{"hooks": [{"type": "command", "command": "use-repo-node \"$CLAUDE_PROJECT_DIR\""}]}])' \
"$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
/opt/node-default/node --version