TL;DR
Claude Code Teams 的 plugin 自動更新預設用 version string 比對——兩端都是同一個版號,永遠不觸發。根本解法是改比 git SHA:任何 commit 都是更新訊號。搭配 GitHub Action 自動 bump 版本+Managed Settings 推 SessionStart hook,團隊成員每次開 new session 就自動拿最新版,零手動操作。
📌 目錄
- 為什麼 plugin 永遠不更新:根因分析
- 先決條件:要讓這套跑起來你需要什麼
- 設定一:把比對邏輯從 version string 改成 git SHA
- 設定二:GitHub Action 自動 bump patch 版本
- 設定三:Managed Settings 推 SessionStart hook
- 驗收:怎麼確認這套真的有用
- 常見問題 FAQ
- 延伸資源
🔍 為什麼 plugin 永遠不更新:根因分析
我自己一開始也以為 Claude Code Teams 的 plugin 只要 push 新 code 就會自動同步。結果用了一段時間,發現版本號一直停在安裝當天那個。
根因很直接:預設的更新邏輯比對的是 version string。
已安裝版本:0.9.11
marketplace 版本:0.9.11
→ 相等,不更新
問題在於:如果你每次 push code 都不手動改 marketplace.json 裡的版本號,兩端永遠相等,更新永遠不觸發。這是一個「邏輯正確但實際上永遠不動」的盲點。
比 version string 更可靠的訊號是 git SHA:任何 commit 都會產生新的 SHA,只要 SHA 不同就代表「有新東西」,就更新。
整個修法涉及三個層面:
| 層面 | 什麼要改 | 效果 |
|---|---|---|
| 更新腳本 | 比 git SHA 取代 version string | 任何 commit 都觸發更新 |
| GitHub Action | 每次 push 自動 bump patch 版本 | marketplace.json 版本號有意義 |
| Managed Settings | 推 SessionStart hook 給所有成員 | 成員不需手動設定 |
---
✅ 先決條件:要讓這套跑起來你需要什麼
在開始設定之前,確認這幾項已就緒:
| 依賴 | 最低需求 | 驗證方式 |
|---|---|---|
| Claude Code | Teams 方案 | claude --version |
| Plugin 已安裝(成員端) | 第一次需手動 /plugin install wez@wezoomtek | ls ~/.claude/plugins/installed_plugins.json |
| Marketplace clone 在本機 | ~/.claude/plugins/marketplaces/wezoomtek/ | ls ~/.claude/plugins/marketplaces/wezoomtek/.claude-plugin/ |
| Python 3 | 腳本用 python3 解析 JSON | python3 --version |
| rsync | 腳本用 rsync 同步檔案 | which rsync |
/plugin install wez@wezoomtek。 本文講的「自動更新」是指「安裝後的版本追蹤」,不是首次安裝流程。
---
⚡ 設定一:把比對邏輯從 version string 改成 git SHA
把下面這支腳本存到你的 plugin repo,路徑是 scripts/update-wez-plugin.sh(同時會被部署到成員的 ~/.claude/plugins/marketplaces/wezoomtek/scripts/)。
#!/bin/bash
# Auto-update wez plugin: compares git SHA, any new commit triggers update
MARKETPLACE_DIR="$HOME/.claude/plugins/marketplaces/wezoomtek"
CACHE_BASE="$HOME/.claude/plugins/cache/wezoomtek/wez"
INSTALLED_JSON="$HOME/.claude/plugins/installed_plugins.json"
# Pull latest from remote
git -C "$MARKETPLACE_DIR" pull --quiet origin main > /dev/null 2>&1
if [ $? -ne 0 ]; then exit 0; fi
# Get latest SHA
REMOTE_SHA=$(git -C "$MARKETPLACE_DIR" rev-parse HEAD 2>/dev/null)
if [ -z "$REMOTE_SHA" ]; then exit 0; fi
# Get currently installed SHA
CURRENT_SHA=$(python3 -c "
import json, sys
try:
d = json.load(open('$INSTALLED_JSON'))
print(d['plugins']['wez@wezoomtek'][0].get('gitCommitSha', ''))
except Exception:
print('')
" 2>/dev/null)
# Exit if already up to date
if [ "$CURRENT_SHA" = "$REMOTE_SHA" ]; then exit 0; fi
# Read version from marketplace.json (for cache dir naming)
TARGET_VERSION=$(python3 -c "
import json
try:
d = json.load(open('$MARKETPLACE_DIR/.claude-plugin/marketplace.json'))
print(d['plugins'][0]['version'])
except Exception:
print('latest')
" 2>/dev/null)
if [ -z "$TARGET_VERSION" ]; then TARGET_VERSION="latest"; fi
# Each commit gets its own cache slot: version-SHA
SHORT_SHA="${REMOTE_SHA:0:8}"
TARGET_CACHE="$CACHE_BASE/${TARGET_VERSION}-${SHORT_SHA}"
mkdir -p "$TARGET_CACHE"
rsync -a --exclude='.git' "$MARKETPLACE_DIR/" "$TARGET_CACHE/" > /dev/null 2>&1
# Update installed_plugins.json
python3 - <<EOF
import json, datetime
path = "$INSTALLED_JSON"
with open(path) as f:
d = json.load(f)
entry = d['plugins']['wez@wezoomtek'][0]
entry['version'] = "$TARGET_VERSION"
entry['installPath'] = "$TARGET_CACHE"
entry['lastUpdated'] = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.000Z')
entry['gitCommitSha'] = "$REMOTE_SHA"
with open(path, 'w') as f:
json.dump(d, f, indent=2, ensure_ascii=False)
EOF
關鍵改動在這兩行:
# 舊做法(version string)
if [ "$CURRENT_VERSION" = "$REMOTE_VERSION" ]; then exit 0; fi
# 新做法(git SHA)
if [ "$CURRENT_SHA" = "$REMOTE_SHA" ]; then exit 0; fi
另外,每次更新後把 gitCommitSha 寫回 installed_plugins.json——下次 session 才能正確比對。
---
⚙️ 設定二:GitHub Action 自動 bump patch 版本
有了 SHA 比對後,marketplace.json 的版本號已不是更新的判斷依據。但版本號還是有價值:方便人看(log、UI 顯示)。所以加一個 GitHub Action 在每次 push main 時自動 bump patch 版本,省去手動改的步驟。
在 plugin repo 建立 .github/workflows/bump-version.yml:
name: Bump plugin version on push
on:
push:
branches: [main]
workflow_dispatch:
jobs:
bump:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Bump patch version in marketplace.json
run: |
FILE=".claude-plugin/marketplace.json"
CURRENT=$(python3 -c "import json; d=json.load(open('$FILE')); print(d['plugins'][0]['version'])")
MAJOR=$(echo $CURRENT | cut -d. -f1)
MINOR=$(echo $CURRENT | cut -d. -f2)
PATCH=$(echo $CURRENT | cut -d. -f3)
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
python3 -c "
import json
with open('$FILE') as f:
d = json.load(f)
d['plugins'][0]['version'] = '$NEW_VERSION'
with open('$FILE', 'w') as f:
json.dump(d, f, indent=2, ensure_ascii=False)
f.write('\n')
"
echo "Bumped $CURRENT → $NEW_VERSION"
- name: Commit version bump
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .claude-plugin/marketplace.json
git diff --cached --quiet || git commit -m "chore: bump plugin version [skip ci]"
git push
一個重要細節:commit message 結尾加 [skip ci]。 這是防止 Action 觸發自己再跑一圈的標準做法。沒加的話,bump-version commit 會再觸發一次 bump-version,無限循環。
另外,不要加 paths-ignore。我踩過這個坑:加了 paths-ignore: ['.github/**'] 之後,pure version bump commit 沒有任何「受監控路徑」有異動,GitHub Actions 判定不需要跑,整個 workflow 靜音消失。移掉 paths-ignore、靠 [skip ci] 控流才是正確做法。
---
🔗 設定三:Managed Settings 推 SessionStart hook
前兩個設定讓更新腳本邏輯正確。但成員要自己在 ~/.claude/settings.json 加 SessionStart hook 才會被觸發——這等於又多了一個手動步驟。
Managed Settings 解決這個問題:Claude Code Teams 管理員可以把 settings 設定推給所有成員,成員端不需要做任何事。
在 Claude Code Teams 後台(admin console)的 Managed Settings 加入以下 JSON:
{
"hooks": {
"SessionStart": [
{
"type": "command",
"command": "bash ~/.claude/plugins/marketplaces/wezoomtek/scripts/update-wez-plugin.sh > /dev/null 2>&1",
"async": true
}
]
}
}
這段的邏輯:
SessionStart:每次開新 Claude Code session 時觸發async: true:背景執行,不阻塞 session 啟動> /dev/null 2>&1:靜默,不污染 session 輸出- 腳本路徑
~/.claude/plugins/marketplaces/wezoomtek/scripts/update-wez-plugin.sh:這支腳本在 plugin 安裝時就已在成員機器上,不需要另外傳送
---
🧪 驗收:怎麼確認這套真的有用
設定完成後,這樣驗收:
驗收步驟一:確認 installed_plugins.json 有 gitCommitSha
cat ~/.claude/plugins/installed_plugins.json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(json.dumps(d['plugins']['wez@wezoomtek'][0], indent=2))
"
正常輸出應包含:
{
"version": "0.9.13",
"gitCommitSha": "d6a837fcab0a0de0c6e8d07a94bb4afaf24b15ce",
"lastUpdated": "2026-06-24T03:05:57.000Z"
}
驗收步驟二:模擬有新版本
把 installed_plugins.json 的 gitCommitSha 改成一個假值(如 aaaaaaaabbbbbbbbcccccccc),然後開新 session 或手動跑腳本:
bash ~/.claude/plugins/marketplaces/wezoomtek/scripts/update-wez-plugin.sh
跑完再查 installed_plugins.json,應該看到 SHA 更新回正確的 remote SHA 值。
驗收步驟三:確認 Managed Settings 有推到成員
在成員機器上查:
cat ~/.claude/settings.json | python3 -c "
import json, sys
d = json.load(sys.stdin)
hooks = d.get('hooks', {}).get('SessionStart', [])
print(json.dumps(hooks, indent=2))
"
看到有 update-wez-plugin.sh 那條就代表 Managed Settings 有生效。
---
❓ 常見問題 FAQ
Q:成員第一次安裝 plugin 還是要手動嗎?
是,第一次需要執行 /plugin install wez@wezoomtek。Managed Settings 管的是「已安裝後的自動更新」,不是首次安裝流程。後續所有更新都是自動的。
Q:Marketplace clone(~/.claude/plugins/marketplaces/wezoomtek/)是從哪來的?
執行 /plugin install wez@wezoomtek 時,Claude Code 會把你指定的 marketplace repo clone 到本機。這個 clone 就是後續 git pull 的來源。腳本用 git -C "$MARKETPLACE_DIR" pull 來拉最新版。
Q:[skip ci] 真的管用嗎?GitHub 說是用 actions/skip?
[skip ci]、[ci skip]、skip-checks: true 都是 GitHub 官方支援的跳過觸發機制,對 push event 有效。詳見 GitHub 文件。
Q:這個腳本跑失敗會怎樣?
腳本在每個關鍵步驟後都有 exit 0(git pull 失敗就退出、SHA 為空就退出)。失敗會靜默退出,不影響 session 啟動,成員不會感覺到任何異常——只是這次沒更新而已。下次 session 再試。
Q:每次 session 都要 git pull 會不會很慢?
git pull --quiet 加上遠端 repo 沒有變動的話,通常 1 秒內。加上 async: true,這個操作不在 session 啟動的關鍵路徑上,實際感受不到延遲。
---
🔗 延伸資源
- Claude Code Teams 官方文件 — Managed Settings 設定方式
- wezoomtek plugin repo — 本文提到的 plugin 原始碼,含完整
update-wez-plugin.sh和 GitHub Action - Claude Code hooks 說明 — SessionStart、PostToolUse 各種 hook 的設定格式
- GitHub Actions:跳過 workflow 觸發 —
[skip ci]官方說明 - Claude Code plugin 系統架構 — 了解 marketplace、installed_plugins.json 結構