Plugin update detection and upgrade workflow

Resolved 💬 3 comments Opened Mar 6, 2026 by mcillkay Closed Mar 10, 2026

Problem

Claude Code plugins installed from marketplace repos have no update mechanism. Users have no way to know when a plugin has upstream updates, and no built-in way to upgrade. This leads to:

  • Running outdated plugins with known bugs (e.g., dependency incompatibilities that are fixed upstream)
  • No visibility into available updates unless users manually check each marketplace repo
  • No standardized upgrade path — users must manually pull repos, rebuild, and update config

Proposed Solution

Two complementary mechanisms:

1. Automatic update detection (SessionStart hook)

A lightweight check that runs at most once per day:

  • Compares each installed plugin's gitCommitSha (from installed_plugins.json) against the remote HEAD of its marketplace repo
  • If updates are available, prints a notice with plugin names and how many commits behind
  • Caches the last-check timestamp to avoid network calls on every session start

2. User-invoked upgrade command

A command (e.g., /upgrade-plugins or claude plugin upgrade) that:

  • Shows available updates with changelog summaries (git log between installed and latest)
  • Asks for confirmation before applying
  • Pulls latest source, runs build steps, updates installed_plugins.json
  • Applies known post-install fixes for specific plugins
  • Reminds the user to restart Claude Code

Reference Implementation

I've built a working version of this as a SessionStart hook + skill. Full source below.

Hook: check-plugin-updates.sh (SessionStart)

Wired in .claude/settings.json:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-plugin-updates.sh",
            "timeout": 15
          }
        ]
      }
    ]
  }
}

Source:

#!/bin/bash
# SessionStart hook: check if installed plugins have upstream updates.
# Runs at most once per day (caches timestamp in ~/.claude/plugins/.last-update-check).
# Prints a notice if any marketplace plugin has new commits.

set -euo pipefail

CACHE_FILE="$HOME/.claude/plugins/.last-update-check"
INSTALLED="$HOME/.claude/plugins/installed_plugins.json"
MARKETPLACES="$HOME/.claude/plugins/known_marketplaces.json"
CHECK_INTERVAL=86400  # 24 hours in seconds

# Skip if no installed plugins file
[ -f "$INSTALLED" ] || exit 0
[ -f "$MARKETPLACES" ] || exit 0

# Staleness check — skip if checked recently
if [ -f "$CACHE_FILE" ]; then
  LAST_CHECK=$(cat "$CACHE_FILE")
  NOW=$(date +%s)
  ELAPSED=$(( NOW - LAST_CHECK ))
  if [ "$ELAPSED" -lt "$CHECK_INTERVAL" ]; then
    exit 0
  fi
fi

# Collect outdated plugins
OUTDATED=()

# Get all marketplace-based plugins (format: "name@marketplace")
PLUGINS=$(jq -r '.plugins | to_entries[] | .key' "$INSTALLED" 2>/dev/null || true)

for PLUGIN_KEY in $PLUGINS; do
  MARKETPLACE=$(echo "$PLUGIN_KEY" | sed 's/.*@//')
  PLUGIN_NAME=$(echo "$PLUGIN_KEY" | sed 's/@.*//')

  # Get marketplace source info
  SOURCE_TYPE=$(jq -r --arg m "$MARKETPLACE" '.[$m].source.source // empty' "$MARKETPLACES" 2>/dev/null || true)
  [ "$SOURCE_TYPE" = "github" ] || continue

  MARKETPLACE_DIR=$(jq -r --arg m "$MARKETPLACE" '.[$m].installLocation // empty' "$MARKETPLACES" 2>/dev/null || true)
  [ -d "$MARKETPLACE_DIR" ] || continue

  # Get installed git SHA
  INSTALLED_SHA=$(jq -r --arg k "$PLUGIN_KEY" '.plugins[$k][0].gitCommitSha // empty' "$INSTALLED" 2>/dev/null || true)
  [ -n "$INSTALLED_SHA" ] || continue

  # Fetch remote and compare (timeout after 5s per repo)
  REMOTE_SHA=$(cd "$MARKETPLACE_DIR" && timeout 5 git fetch origin --quiet 2>/dev/null && git rev-parse origin/HEAD 2>/dev/null || true)
  [ -n "$REMOTE_SHA" ] || continue

  if [ "$INSTALLED_SHA" != "$REMOTE_SHA" ]; then
    INSTALLED_VER=$(jq -r --arg k "$PLUGIN_KEY" '.plugins[$k][0].version // "?"' "$INSTALLED" 2>/dev/null || true)
    # Count commits behind
    BEHIND=$(cd "$MARKETPLACE_DIR" && git rev-list --count "$INSTALLED_SHA".."$REMOTE_SHA" 2>/dev/null || echo "?")
    OUTDATED+=("$PLUGIN_NAME ($INSTALLED_VER, ${BEHIND} commits behind)")
  fi
done

# Update cache timestamp
mkdir -p "$(dirname "$CACHE_FILE")"
date +%s > "$CACHE_FILE"

# Report
if [ ${#OUTDATED[@]} -gt 0 ]; then
  MSG="Plugin updates available:\\n"
  for P in "${OUTDATED[@]}"; do
    MSG+="  - $P\\n"
  done
  MSG+="Run /upgrade-plugins to update."

  jq -n --arg msg "$MSG" '{"systemMessage": $msg}'
fi

Skill: upgrade-plugins/SKILL.md

---
name: upgrade-plugins
description: Check for and apply plugin updates from marketplace repos. Pulls latest source, rebuilds if needed, and updates installed_plugins.json.
allowed-tools:
  - Read
  - Bash(git:*)
  - Bash(cd:*)
  - Bash(jq:*)
  - Bash(cat:*)
  - Bash(npm install)
  - Bash(npm run build)
  - Bash(cp:*)
  - Bash(rm:*)
  - Bash(mkdir:*)
  - Bash(ls:*)
  - Bash(date:*)
  - Bash(node:*)
  - Bash(which:*)
  - Bash(make install:*)
  - Bash(SKIP_UPDATE_CHECK=1:*)
  - Glob
  - Write
  - Edit
---

# Upgrade Plugins

Check all installed marketplace plugins for updates and apply them.

## Instructions

1. Read ~/.claude/plugins/installed_plugins.json and ~/.claude/plugins/known_marketplaces.json.

2. For each installed plugin backed by a **github** marketplace:
   a. cd to the marketplace install location
   b. Run git fetch origin and compare the installed gitCommitSha against origin/HEAD
   c. If they differ, report the plugin as **upgradable** with the number of commits behind
   d. Show a brief git log --oneline <installed_sha>..origin/HEAD summary of what changed

3. **Present the list to the user** and ask for confirmation before upgrading. Show:
   - Plugin name and current version
   - Number of commits behind
   - Changelog summary (from git log)

4. On confirmation, for each plugin to upgrade:
   a. cd to the marketplace directory and git pull origin (merge, not rebase)
   b. Read the new package.json version
   c. Create the new cache directory: ~/.claude/plugins/cache/<marketplace>/<plugin>/<new_version>/
   d. Copy the marketplace contents to the new cache directory
   e. Run build steps based on plugin type:
      - **Cortex**: npm install && npm run build
        - After install, check if @xenova/transformers/node_modules/sharp exists and remove it
          (forces use of top-level sharp compatible with Node v25)
        - Add overrides to package.json if not present, then re-run npm install
      - **Beads**: SKIP_UPDATE_CHECK=1 make install (if Go is available)
      - **Other**: Check for package.json scripts and run npm install && npm run build if present
   f. Update installed_plugins.json:
      - Set new version, installPath, lastUpdated, and gitCommitSha

5. **Reset the update check cache**: Write current timestamp to ~/.claude/plugins/.last-update-check

6. **Summary**: Report what was upgraded, what failed, and whether a restart is needed.
   - **Always remind**: "Restart Claude Code to load the updated plugins."

## If No Updates Available

Report that all plugins are up to date with their marketplace repos.

Why This Should Be Native

Plugin update checking is infrastructure — it belongs in the CLI itself rather than as a user-maintained hook. The CLI already tracks gitCommitSha and marketplace sources in installed_plugins.json, so it has all the data needed. A native claude plugin upgrade command would be more reliable and consistent than per-project hooks.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗