Last active
April 20, 2026 14:34
-
-
Save marceldarvas/9e10fd41d608bdb1ba277b7f989b4763 to your computer and use it in GitHub Desktop.
Pin Claude Code to a specific npm version (bypassing Homebrew) or restore to Homebrew-managed latest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Required parameters: | |
| # @raycast.schemaVersion 1 | |
| # @raycast.title Claude Code Version Pin | |
| # @raycast.mode fullOutput | |
| # @raycast.argument1 { "type": "dropdown", "placeholder": "Action", "data": [{"title": "Pin", "value": "pin"}, {"title": "Restore", "value": "restore"}] } | |
| # Optional parameters: | |
| # @raycast.icon 🔧 | |
| # Documentation: | |
| # @raycast.description Pin Claude Code to a specific npm version or restore to original install method (Homebrew / self-managed). | |
| # @raycast.author marceldarvas | |
| # @raycast.authorURL https://raycast.com/marceldarvas | |
| # --- Configuration --- | |
| PINNED_VERSION="2.1.66" | |
| ZSHRC="$HOME/.zshrc" | |
| AUTO_UPDATE_VAR="CLAUDE_CODE_DISABLE_AUTO_UPDATE" | |
| MARKER="# claude-code-version-pin managed" | |
| STATE_FILE="$HOME/.claude-code-pin-state" | |
| set -o pipefail | |
| # --- Helpers --- | |
| info() { echo "ℹ️ $*"; } | |
| ok() { echo "✅ $*"; } | |
| warn() { echo "⚠️ $*"; } | |
| fail() { echo "❌ $*"; exit 1; } | |
| current_version() { | |
| claude --version 2>/dev/null | head -1 | |
| } | |
| # Detect how Claude Code is currently installed | |
| # Returns: "homebrew", "self-managed", "npm", or "unknown" | |
| detect_install_method() { | |
| local claude_path | |
| claude_path=$(which claude 2>/dev/null) | |
| if [ -z "$claude_path" ]; then | |
| echo "unknown" | |
| return | |
| fi | |
| # Resolve symlinks | |
| local resolved | |
| resolved=$(readlink -f "$claude_path" 2>/dev/null || readlink "$claude_path" 2>/dev/null || echo "$claude_path") | |
| # Check Homebrew | |
| if brew list claude-code &>/dev/null && [[ "$resolved" == *"Cellar"* || "$resolved" == *"homebrew"* ]]; then | |
| echo "homebrew" | |
| # Check self-managed (~/.local/share/claude/versions/) | |
| elif [[ "$resolved" == *".local/share/claude/versions"* ]]; then | |
| echo "self-managed" | |
| # Check npm global | |
| elif [[ "$resolved" == *"node_modules"* || "$resolved" == *"npm"* || "$resolved" == *"lib/node_modules"* ]]; then | |
| echo "npm" | |
| # Fallback: check if Homebrew owns it even without Cellar in path | |
| elif brew list claude-code &>/dev/null; then | |
| echo "homebrew" | |
| else | |
| echo "unknown" | |
| fi | |
| } | |
| pin() { | |
| info "Current version: $(current_version)" | |
| # 1. Detect current install method and save for restore | |
| INSTALL_METHOD=$(detect_install_method) | |
| info "Detected install method: ${INSTALL_METHOD}" | |
| echo "$INSTALL_METHOD" > "$STATE_FILE" | |
| ok "Saved original install method to ${STATE_FILE}" | |
| # 2. Handle current install method | |
| case "$INSTALL_METHOD" in | |
| homebrew) | |
| info "Unlinking Homebrew claude-code..." | |
| brew unlink claude-code || fail "Failed to unlink Homebrew claude-code" | |
| ok "Homebrew claude-code unlinked (formula kept for later restore)" | |
| ;; | |
| self-managed) | |
| info "Self-managed install detected at ~/.local/share/claude/versions/" | |
| info "npm install will take PATH priority once auto-update is disabled" | |
| ;; | |
| npm) | |
| info "Already npm-managed, will overwrite with pinned version" | |
| ;; | |
| *) | |
| warn "Could not determine install method — proceeding anyway" | |
| ;; | |
| esac | |
| # 3. Install pinned version via npm | |
| info "Installing claude-code@${PINNED_VERSION} via npm..." | |
| npm install -g "@anthropic-ai/claude-code@${PINNED_VERSION}" || fail "npm install failed" | |
| ok "Installed claude-code@${PINNED_VERSION}" | |
| # 4. Disable auto-update in .zshrc | |
| if grep -q "$AUTO_UPDATE_VAR" "$ZSHRC" 2>/dev/null; then | |
| info "Auto-update disable already present in .zshrc" | |
| else | |
| info "Adding auto-update disable to .zshrc..." | |
| echo "" >> "$ZSHRC" | |
| echo "export ${AUTO_UPDATE_VAR}=1 ${MARKER}" >> "$ZSHRC" | |
| ok "Auto-update disabled" | |
| fi | |
| # Export for current shell too | |
| export "${AUTO_UPDATE_VAR}=1" | |
| # 5. Verify | |
| info "Verifying installation..." | |
| WHICH=$(which claude 2>/dev/null) | |
| VERSION=$(current_version) | |
| ok "claude binary: ${WHICH}" | |
| ok "Version: ${VERSION}" | |
| echo "" | |
| ok "Claude Code pinned to ${PINNED_VERSION}" | |
| info "Restart any open Claude sessions to pick up the new version." | |
| info "Run with 'restore' once the fix ships to return to ${INSTALL_METHOD} management." | |
| } | |
| restore() { | |
| info "Current version: $(current_version)" | |
| # 1. Determine original install method | |
| if [ -f "$STATE_FILE" ]; then | |
| ORIGINAL_METHOD=$(cat "$STATE_FILE") | |
| info "Original install method (from state file): ${ORIGINAL_METHOD}" | |
| else | |
| ORIGINAL_METHOD=$(detect_install_method) | |
| warn "No state file found — detected current method: ${ORIGINAL_METHOD}" | |
| fi | |
| # 2. Remove npm global version | |
| info "Removing npm global claude-code..." | |
| npm uninstall -g @anthropic-ai/claude-code 2>/dev/null | |
| ok "npm claude-code removed" | |
| # 3. Restore based on original install method | |
| case "$ORIGINAL_METHOD" in | |
| homebrew) | |
| info "Restoring Homebrew-managed claude-code..." | |
| if brew list claude-code &>/dev/null; then | |
| brew link claude-code || fail "Failed to link Homebrew claude-code" | |
| else | |
| brew install claude-code || fail "Homebrew install failed" | |
| fi | |
| ok "Homebrew claude-code restored" | |
| ;; | |
| self-managed) | |
| info "Restoring self-managed claude-code..." | |
| # The self-managed binary at ~/.local/bin/claude should already be on PATH | |
| # once npm version is removed. Auto-update will resume and handle versioning. | |
| if [ -L "$HOME/.local/bin/claude" ] || [ -f "$HOME/.local/bin/claude" ]; then | |
| ok "Self-managed binary still present at ~/.local/bin/claude" | |
| info "Auto-update will bring it to latest on next launch" | |
| else | |
| warn "Self-managed binary not found — you may need to reinstall Claude Code" | |
| info "Reinstall: curl -fsSL https://claude.ai/install.sh | sh" | |
| fi | |
| ;; | |
| npm) | |
| info "Reinstalling latest claude-code via npm..." | |
| npm install -g @anthropic-ai/claude-code || fail "npm install failed" | |
| ok "npm claude-code restored to latest" | |
| ;; | |
| *) | |
| warn "Unknown original method '${ORIGINAL_METHOD}' — leaving npm uninstalled" | |
| info "Reinstall manually via your preferred method" | |
| ;; | |
| esac | |
| # 4. Remove auto-update disable from .zshrc | |
| if grep -q "$MARKER" "$ZSHRC" 2>/dev/null; then | |
| info "Removing auto-update disable from .zshrc..." | |
| sed -i '' "/${MARKER}/d" "$ZSHRC" | |
| # Clean up any trailing blank line left behind | |
| sed -i '' -e :a -e '/^\n*$/{$d;N;ba' -e '}' "$ZSHRC" | |
| ok "Auto-update re-enabled" | |
| fi | |
| unset "$AUTO_UPDATE_VAR" | |
| # 5. Clean up state file | |
| rm -f "$STATE_FILE" | |
| # 6. Verify | |
| info "Verifying installation..." | |
| WHICH=$(which claude 2>/dev/null) | |
| VERSION=$(current_version) | |
| ok "claude binary: ${WHICH}" | |
| ok "Version: ${VERSION}" | |
| echo "" | |
| ok "Claude Code restored to ${ORIGINAL_METHOD} management" | |
| info "Restart any open Claude sessions to pick up the new version." | |
| } | |
| # --- Main --- | |
| ACTION="${1:-}" | |
| case "$ACTION" in | |
| pin) pin ;; | |
| restore) restore ;; | |
| *) | |
| echo "Usage: $(basename "$0") <pin|restore>" | |
| echo "" | |
| echo " pin — Downgrade to claude-code@${PINNED_VERSION} via npm, disable auto-update" | |
| echo " restore — Remove npm version, restore Homebrew, re-enable auto-update" | |
| exit 1 | |
| ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment