寫於 2026 年 8 月 24 日,9 月才上線(部落格的發佈額度在 8 月中用完了,稿子積壓了三週)。文中的版本號與「目前」都指 8 月 24 日的官方文件狀態,Claude Code 更新很快,你讀到時可能已經又改過了。

你的 Claude Code 會建議你裝哪個外掛,是兩群不在你電腦前面的人決定的。一群是寫你剛剛跑的那個 CLI 的維護者,另一群是幫你公司寫 managed settings 的人。使用者在這件事上只有兩個動作可以做:按 Enter,或者不按。

這兩群人走的是兩條完全不同的通道,機制上沒有一行共用的邏輯。一條靠你的 CLI 往 stderr 吐一行標記,另一條靠 marketplace 條目宣告訊號、由 Claude Code 在本機比對當前 session。兩條都不會自動安裝任何東西。除了這一點以外,它們的觸發時機、能出現在哪裡、一輩子能出現幾次、誰有權打開它,全部不一樣。

一行標記,只有殼看得到

通道 A 的協定本體就一行,自閉合標籤:

1
<claude-code-hint v="1" type="plugin" value="example-cli@claude-plugins-official" />

三個屬性都必填。v 是協定版本,目前只支援 1type 目前只支援 pluginvaluename@marketplace 形式的 plugin 識別字。屬性值可以加雙引號也可以不加,不加的時候不能含空白,而且不支援轉義序列。

這一行要吐到 stderr,而且不能無條件吐。真人直接跑你的 CLI 的時候,他不該看到一串裸標籤。官方的做法是拿環境變數當開關:

1
2
3
4
5
6
// Node.js
if (process.env.CLAUDECODE) {
process.stderr.write(
'<claude-code-hint v="1" type="plugin" value="example-cli@claude-plugins-official" />\n',
)
}

Python 跟 Shell 是同一個形狀,判斷式換個寫法而已:

1
2
3
4
5
6
7
# Python
import os, sys
if os.environ.get("CLAUDECODE"):
print(
'<claude-code-hint v="1" type="plugin" value="example-cli@claude-plugins-official" />',
file=sys.stderr,
)
1
2
3
4
# Shell
if [ -n "$CLAUDECODE" ]; then
printf '%s\n' '<claude-code-hint v="1" type="plugin" value="example-cli@claude-plugins-official" />' >&2
fi

Go 版官方也給了,判斷式是 os.Getenv("CLAUDECODE") != "",輸出走 fmt.Fprintln(os.Stderr, ...)

這裡有個取捨,決定了會不會有真人看到你的裸標籤。CLAUDECODE 每個版本都會設,觸及的 session 最多,代價是它設得太廣:tmux session、stdio MCP server 的子行程、IDE 擴充的整合終端機都有這個變數。另一個選擇是 CLAUDE_CODE_CHILD_SESSION。它只在 Claude Code 自己 spawn 的子行程裡設(tool call、hook、statusline),正常不會流到真人的終端機。代價是需要 v2.1.172+,舊版 session 收不到。而且從 session 裡啟動的長壽行程會把變數帶下去,tmux server 就是典型,之後從它開的 shell 一樣會看到裸標籤。

兩個都會漏。挑哪個,看你的使用者比較怕看到什麼。

Claude Code 掃到這一行之後做四件事:把它從輸出裡拿掉(在送進模型之前,所以它不佔 token)、確認目標 plugin 在官方 Anthropic marketplace、確認這個 plugin 沒裝過也沒提示過、然後才顯示安裝提示。提示裡會寫出是哪個指令吐出這個 hint,讓使用者看得出工具跟它推薦的東西對不對得上。

官方建議的落點有四個:--help 輸出(Claude 探索陌生 CLI 的時候常跑)、未知子指令的錯誤訊息、登入或認證成功、首次執行的歡迎訊息。第二個是我看下來最漂亮的位置。使用者打錯子指令,正好是 Claude 最搞不懂你這個介面的時候。它正在找路,你這時候把 plugin 遞過去。去重是以 plugin 為單位做的,所以「每次呼叫都吐」不會有壞處。

兩個硬性要求別忘:標籤必須獨佔一行,前後留白可以,夾在 log 語句中間會被忽略;value 必須指向 Anthropic 控制的 marketplace,指向別的地方會被靜默丟棄。版本或 type 不認得的也照樣會被剝掉。

發得出去,不等於出得來

上面那四件事裡,第二件才是整條通道真正的門。hint 只對官方 marketplace claude-plugins-official 生效,而誰能進那份名單是 Anthropic 自己決定的。app 裡的送審表單是進社群 marketplace,hint 協定不檢查社群 marketplace。要上官方名單,文件寫的是透過 Anthropic 的合作夥伴聯絡人協調。

如果你是一個人維護自己的 CLI,通道 A 對你實際上是關著的。這件事值得早點知道,免得花一個下午把四種語言的 hint 都接好,最後發現卡在一封信。

還有幾條會讓你白做工的邊界。只有 Bash 與 PowerShell tool 的輸出會觸發安裝提示,hook 指令裡的 hint 會被剝掉並忽略。subagent 跑的指令、claude -p 非互動模式、Agent SDK 一律不提示,hint 行照樣會被剝掉。

另一條通道,鑰匙在管理員身上

通道 B 的發話權在 marketplace 營運者手上,寫法是在條目裡加一個 relevance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "acme-corp-plugins",
"owner": { "name": "Acme Platform Team" },
"plugins": [
{
"name": "terraform-helpers",
"source": "./plugins/terraform-helpers",
"description": "Acme conventions and helpers for Terraform",
"relevance": {
"topic": "Terraform",
"signals": {
"cli": ["terraform"],
"filesRead": ["**/*.tf"]
}
}
}
]
}

topic 上限 64 字元,省略的時候預設是「plugin 名稱、每個連字號分段首字母大寫」。整個功能需要 Claude Code v2.1.152+,舊版會直接忽略 relevance 欄位。

寫完這段,什麼事都還不會發生。要讓它出現,得在 managed settings 把這個 marketplace 加進 pluginSuggestionMarketplaces。而且非官方 marketplace 還要在同一份 managed settings 裡宣告來源,用 extraKnownMarketplacesstrictKnownMarketplaces,否則允許名單上那個名字會被忽略:

1
2
3
4
5
6
7
8
9
// managed-settings.json:註冊自家 GitHub marketplace 並開啟建議
{
"extraKnownMarketplaces": {
"acme-corp-plugins": {
"source": { "source": "github", "repo": "acme-corp/claude-plugins" }
}
},
"pluginSuggestionMarketplaces": ["acme-corp-plugins"]
}

官方 marketplace 是例外,那個名字只可能從官方來源註冊,所以只要允許名字就好:

1
{ "pluginSuggestionMarketplaces": ["claude-plugins-official"] }

為什麼要疊兩層,文件講得很直白:防止不相關的來源用允許名單上的名字註冊,然後讓自己的 plugin 在全組織被推薦。名字是可以搶的,來源不行。managed settings 在設定檔的層級關係裡有多硬,之前拆過一次(設定檔為什麼長成五層),這裡剛好又用上它。

cd infra && terraform plan 只留下一個 cd

signals 有五種,這是全篇最容易照文件寫還踩坑的地方。

先講那個坑。cli 比對的是本 session 跑過的 shell 指令名,但每一次 shell tool 呼叫只記一個:去掉前置環境變數指派與 sudo 之後的第一個 token。所以 cd infra && terraform plan 記到的是 cd,不是 terraform。你把 cli: ["terraform"] 當主要 signal,然後在 IaC 專案裡跑一整天,可能一次都不會觸發。

filesRead: ["**/*.tf"] 才穩。那個 signal 連 Claude 寫過、編輯過的檔案都算,還包含自動載入的 CLAUDE.md

五種 signal 的比對對象與上限:

signal 比對對象 上限
cwd session 工作目錄的 glob 10 個 pattern,各 256 字元
cli 本 session 跑過的 shell 指令名,精確比對 10 個,各 64 字元
hosts Bash 指令裡 http(s):// URL 的主機名 20 個,各 128 字元
filesRead Claude 本 session 讀過/寫過的檔案路徑 glob 10 個 pattern,各 256 字元
manifestDeps 讀過的套件 manifest 內容 10 筆,各 256 字元

幾個細節。cwd 同時以絕對路徑、以及(在 git repo 裡的時候)相對 repo root 的路徑比對。正斜線正規化、不分大小寫,infrainfra/infra/** 三種寫法行為完全相同。hosts 只吃純小寫主機名,不含 scheme、port 與 path,做不分大小寫的精確比對。

manifestDeps 是唯一要寫正規式的那個,形狀是 { "file": 正規式, "pattern": 正規式 }file 一定要尾錨定。文件給的例子是 [/\\\\]package\\.json$,頭錨定的 pattern 永遠比不到絕對路徑。

還有一條時序上的分野,會直接決定你的建議出現在哪。cwd 是唯一能在第一回合之前就命中的 signal,因為工作目錄一開始就知道。另外四種都需要 session 歷史,它們只能出現在 spinner tip 與 /plugin 的 Discover 分頁,不可能在 session 開場命中。

signals 至少要有一個才算「可被建議」。有 relevance 但沒有任何 signal 命中的 plugin,行為跟普通條目完全一樣。發佈前記得跑 claude plugin validate ./my-marketplace,它會把 relevancerelevance.signals 下面的未知鍵報成 warning,把非物件的 relevance 標出來,並且直接拒絕含 scheme、port 或 path 的 signals.hosts 條目。

出現幾次也不是你決定的

建議會出現在三個位置。spinner tip 是 Claude 回應的時候,spinner 下方冒出一行 Working with Terraform? Install the terraform-helpers plugin: 加上 /plugin install ...。開場提示是 cwd 命中時、第一回合之前的那一行 plugin suggestion: terraform-helpers@acme-corp-plugins · /plugin,需要 v2.1.153+,而且這一行不使用 topic。第三個是 /plugin 的 Discover 分頁,命中的 plugin 會釘在最上面,標注 suggested for this directorysuggested for terraform commands,需要 v2.1.154+,只釘一次,之後回到正常排序。

頻率上限這件事,寫外掛的人自己量不出來,但文件寫得很清楚。relevance 這邊:同一個 plugin 的建議在 spinner tip 與開場提示合計每三個 session 最多一次;開場那行顯示兩次之後就不再出現;裝了就都不再出現。

CLI hint 那邊更嚴:每個 plugin 一輩子只提示一次,不管使用者答什麼;每個 session 全機器最多一次;只在使用者正在打字的主互動 session 出現;三十秒沒回應視為 No。

一輩子一次。你連「再問一次」的權利都沒有。

唯一在使用者手上的開關

使用者手上還剩什麼?兩個開關,形狀很不一樣。

明的那個:spinner tip 與開場提示都吃 spinnerTipsEnabled: false(或是設了帶 excludeDefaultspinnerTipsOverride),設下去整組關掉,Discover 分頁的釘選不受 tip 設定影響。你關掉它,你知道自己關掉了什麼。

暗的那個藏在遙測裡。CLI hint 在關閉分析的 session 完全不會出現:設了 DISABLE_TELEMETRYCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC 的 session 不提示,走 Amazon Bedrock、Google Cloud Agent Platform 這類自動 opt-out 遙測的第三方供應商也不提示。

一個產品建議機制被綁在遙測開關上,代表在實作上它被歸類成非必要流量。企業把遙測關掉通常是為了資安合規,順手關掉的還有一整條 plugin 發現通道,而按下那個開關的人多半沒意識到。反過來看通道 B 就是對比組:relevance 的 signal 比對完全在本機做,不產生網路流量,也不會把「哪個 signal 命中、它的值是什麼」回報給 Anthropic 或 marketplace 營運者。

我沒跑過的部分

這篇從頭到尾只讀官方文件。我沒有發出過一行 hint,沒有把 relevance 放上任何 marketplace,也沒有 Team 或 Enterprise 的管理員權限可以改 managed settings 去驗證那個允許名單真的擋得住東西。上面所有版本號、字元上限與比對規則都是文件寫的,不是我量出來的。

文件也有沒寫的地方。使用者端有沒有辦法逐一關掉某個 marketplace 的建議,文件沒有說明,我只找到全域的 spinnerTipsEnabled。relevance 的 signal 比對有沒有效能成本,文件同樣沒寫。這兩點我不猜。

四個問題

拆完之後真正帶得走的不是那一行標籤的寫法,而是一組問法。下次遇到任何「系統主動開口建議你做某件事」的功能,照順序問四個問題:誰有資格發話、誰有權讓它出現、它最多能出現幾次、誰能整條關掉。

如果你正在寫一個要被 Claude 拿去用的 CLI,這四個問題裡你只握著第一個。剩下三個,找對的人談比寫對的 code 重要。

來源

  • Recommend your plugin from your CLI:https://code.claude.com/docs/en/plugin-hints.md
  • Recommend plugins for your org:https://code.claude.com/docs/en/plugin-relevance.md