你的外掛依賴別人的外掛,Claude Code 怎麼決定要裝哪一版
寫於 2026 年 8 月 18 日,9 月才上線(部落格的發佈額度在 8 月中用完了,要等 9 月 1 日重置才發得出去)。文中的版本號與「目前」都指 8 月 18 日的狀態,Claude Code 更新很快,你讀到時可能已經又改過了。
1 | Dependency "secrets-vault@acme-tools" has no git tag satisfying ~2.1.0 |
這行錯誤訊息很短,但它把 Claude Code 解析外掛依賴的整套機制都露出來了。裡面有四個資訊:依賴的名字、它屬於哪個 marketplace、版本要求寫成 semver range,還有最關鍵的那個字,tag。這裡沒有註冊中心,版本號是從 git tag 讀出來的。
把這件事拆開來看,比先講「這個功能可以幹嘛」有用。因為依賴解析這種東西,只要你不知道它底層怎麼算,用起來就會在某天早上收到一個看不懂的錯誤。
第一層:依賴寫在哪,長什麼樣
檔案是外掛自己的 .claude-plugin/plugin.json,欄位叫 dependencies,是個陣列:
1 | { |
陣列裡可以放兩種東西。放裸字串,官方的說法是這樣:
An entry can be a bare string with only the plugin name, like
"audit-logger"in the example above, which depends on whatever version that plugin’s marketplace provides.
裸字串等於「隨便,marketplace 給我什麼版本我都收」。放物件就能加約束,物件有三個欄位:
name 是外掛名,預設在同一個 marketplace 裡找。version 是 semver range,~2.1.0、^2.0、>=1.4、=2.1.0 都合法。marketplace 用來指到別的 marketplace,但這條路預設是封住的,等一下講。
version 那欄的解析規則照抄原文:
The dependency is fetched at the highest tagged version that satisfies this range.
滿足 range 的最高版本。還有一條容易忽略的:pre-release 預設被排除,除非你的 range 自己寫進 pre-release 後綴,像 ^2.0.0-0 這樣。這代表你在 beta 的依賴不會被意外裝進去,但也代表你想測 beta 就得明確寫出來。
第二層:版本從哪裡來
前面那行錯誤訊息說 has no git tag satisfying,所以版本的來源就是 tag。tag 怎麼命名,這是硬規定:
Tag each release as
{plugin-name}--v{version}, where{version}matches the version field in that commit’splugin.json.
兩個連字號,不是一個。secrets-vault--v2.1.0 這樣。這個格式看起來很囉唆,但一個 repo 裡可能放好幾個外掛,tag 得帶名字才分得開。
有指令幫你做:
1 | claude plugin tag --push |
它不只是 git tag 的包裝。跑下去之前它會驗外掛內容、檢查 plugin.json 的版本跟 marketplace entry 對不對得上、要求外掛目錄的工作區是乾淨的、tag 已經存在就直接拒絕。成功的話最後兩行是 Created tag secrets-vault--v2.1.0 跟 Pushed to origin。
不加 --push 它會印出你該自己跑的 git push 指令;--dry-run 只印不做;要推到別的 remote 用 --remote。等價的手動寫法就是 git tag secrets-vault--v2.1.0,指令幫你擋的是那幾個檢查。
發版前先跑 --dry-run 看一眼 tag 名稱是划算的,因為版本號打錯這件事,等別人裝到 no-matching-tag 才發現就太晚了。
第三層:兩個外掛同時要求同一個依賴
這一層是整套機制裡最值得記的。假設 A 跟 B 都依賴 secrets-vault,但要求的 range 不一樣,Claude Code 會去算交集。官方給了三種結果:
| Plugin A requires | Plugin B requires | Result |
|---|---|---|
^2.0 |
>=2.1 |
One install at the highest 2.x tag at or above 2.1.0. Both plugins load. |
~2.1 |
~3.0 |
Install of plugin B fails with range-conflict. Plugin A and the dependency stay as they were. |
=2.1.0 |
none | The dependency stays at 2.1.0. Auto-update skips newer versions while plugin A is installed. |
第一列是好狀況:兩個 range 有交集,只裝一份,兩個外掛都活。
壞狀況在第二列,而且壞得很有紀律。~2.1 跟 ~3.0 沒有交集,此時失敗的是後裝的那個,而 A 跟依賴維持原狀。這個設計值得停下來想一秒:它選擇讓新的安裝失敗,而不是為了滿足 B 去動已經能跑的 A。已經在跑的東西不會因為你裝了一個新玩具而壞掉。
第三列是釘死版本的副作用。你寫 =2.1.0,auto-update 在你這個外掛還裝著的期間就會跳過那個依賴的新版。想鎖就鎖得住,代價是整台機器上這個依賴都跟著不動。
順著這條線還有一個保護:想停用被依賴的外掛,Claude Code 會擋你,而且錯誤訊息把指令串好給你:
1 | secrets-vault is still required by deploy-kit. Disable that plugin first, or |
第四層:跨 marketplace 的信任不會鏈式傳遞
依賴預設在同一個 marketplace 裡解析。要跨出去,得在根 marketplace 的 .claude-plugin/marketplace.json 裡開白名單:
1 | { |
「根 marketplace」的定義是關鍵,原文寫得很清楚:
The root marketplace is the one that hosts the plugin the user is installing; only its allowlist is consulted, so trust does not chain through intermediate marketplaces.
只看使用者實際安裝的那個 marketplace 的白名單。中間層信任了誰,不算。
這條規則的形狀跟門禁卡很像。你刷卡進大樓,管理室只認你這張卡的權限;你朋友在裡面幫你開門這件事,不會讓你自動有進機房的權限。沒開白名單就裝的話,安裝會失敗並且錯誤訊息會直接點名你該去設哪個欄位。
第五層:孤兒依賴與四種錯誤碼
被自動裝進來的依賴,在上游外掛移除後會變孤兒。清掉它們:
1 | claude plugin prune |
或是移除的時候順手清:
1 | claude plugin uninstall deploy-kit --prune |
有兩個行為要知道。第一,沒東西可清的時候它會印 Nothing to prune 加上原因就結束,官方特別註明這是全新安裝下的預期輸出,不是錯誤。第二,你自己手動裝的外掛永遠不會被 prune 掉,它只清那些透過別人的 dependencies 自動裝進來的。旗標有 --scope project、--scope local、--dry-run、-y;非 TTY 環境不加 -y 就只列不刪。
錯誤碼一共四種,讀懂它們比背指令有用:
dependency-unsatisfied 是依賴沒裝,或者裝了但被停用。range-conflict 是版本要求沒辦法合併,訊息會說明原因是「沒有版本滿足所有 range」、「range 的 semver 語法無效」還是「合起來太複雜算不出交集」。dependency-version-unsatisfied 是裝的那個版本掉在你宣告的 range 外面。no-matching-tag 就是開頭那行錯誤,repo 裡沒有滿足 range 的 {name}--v* tag。
要程式化地檢查,跑 claude plugin list --json。有問題的外掛會多一個 errors 欄位;乾淨載入的外掛不會有這個欄位,不是給你一個空陣列。寫檢查腳本的時候這個差別會咬人。
最後一個例外要記著:npm、archive、command 這幾種來源的依賴,version 約束管不到抓哪一版,因為 tag 解析只對 git 來源有效。版本不合就把外掛停用並標 dependency-version-unsatisfied。command 來源的依賴 Claude Code 永遠不會幫你裝。
拆完了,回頭看它能做什麼
機制看完,用途反而變得很好推。
最直接的一個是收斂共用層。我自己維護的 CREW marketplace 裡有 bug-workflow 跟 feature-workflow 兩個外掛,共用的 Notion 設定與 ID 對照散在兩邊,改一次要記得改兩處。照上面的機制,把共用部分抽成 crew-shared,兩個外掛都宣告 { "name": "crew-shared", "version": "~1.2.0" },共用層發 patch 版兩邊自動吃到,發 minor 版就得明確改 range,不會靜默地一次弄壞兩邊。
第二個用法比較意外:一個只有 dependencies 的外掛,本身就是一包團隊標準配備。
1 | { |
官方明講 manifest 除了必填的 name 之外可以只有一個 dependencies 陣列。新人 onboarding 從此是一行指令,之後團隊多加工具就發 bundle 的新版。
要讓成員真的拿到新版有個前提:非 Anthropic 的 marketplace 預設關閉 auto-update。所以要嘛在 /plugin 裡把那個 marketplace 的 auto-update 開起來,要嘛請大家跑 claude plugin update backend-standard 再 /reload-plugins。全公司鋪開的做法是把 bundle 外掛寫進 managed settings 的 enabledPlugins。
邊界
版本要求我只查到一條明確的:本地資料夾型 marketplace 的 tag 解析需要 Claude Code v2.1.196 或更新。dependencies 欄位、claude plugin tag、claude plugin prune、allowCrossMarketplaceDependenciesOn 各自從哪個版本開始有,官方文件沒標,我沒查證到。文件裡也沒看到 beta 字樣,但我同樣沒查證它是不是已經 GA。
另外老實說:上面的機制我是讀文件拆出來的,crew-shared 那個重構我還沒動手做。等真的把共用層抽出去、踩到第一個 range-conflict,會有更值得寫的東西。
那個「兩個外掛要求打架時,讓後裝的失敗」的設計選擇,我猜是從其他套件管理器的歷史學來的。要驗證這個猜測,得去翻他們的設計討論,這篇沒查。
一張帶得走的圖
整套機制縮成三句就記得住:版本從 git tag 讀,多個約束取交集,孤兒依賴要自己掃。
門禁卡那個類比可以一路帶到最後。你的外掛能不能拿到它要的依賴,過的是三道門:tag 在不在、range 合不合得起來、跨 marketplace 的白名單上有沒有你的名字。卡在哪一道,訊息都會講:前兩道分別是 no-matching-tag 與 range-conflict,第三道則是一則會點名你該去設哪個欄位的安裝錯誤。
今天能做的最小實驗只要一行:在已經裝了幾個外掛的機器上跑一次 claude plugin list --json,拿結果對照上面那四個錯誤碼。比讀文件快得多。

























































































































































































