Plugin update detection and upgrade workflow
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(frominstalled_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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗