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

你打 /deploy,跟 Claude 自己判斷「這段程式碼看起來可以上了」然後去跑 deploy,這兩件事底層到底在幹嘛?

直覺會說:同一個 skill、同一份 SKILL.md、同一段指令,所以是同一條路,差別只在誰按下按鈕。

實際上 Claude Code 手上握著兩份不同的東西。一份是給模型讀的清單,另一份是名字怎麼對到檔案的規則。frontmatter 裡那幾個欄位看起來像在設定「這個 skill 的權限等級」,實際上它們各自去劃掉這兩份東西裡的某一行。

把這兩份先拆開,欄位的行為就全部自動解釋了,連那些看起來很怪的邊角行為也是。

第一份:給模型讀的清單,而且有頁數上限

Claude 不會每次都去掃你的 ~/.claude/skills/。開一個 session 的時候,Claude Code 把所有 skill 的名字跟描述組成一份清單塞進 context,模型知道的就只有這份清單。skill 的正文(也就是 SKILL.md 的內容)在被叫起來之前完全不在裡面。

所以「Claude 會不會自己用你的 skill」這件事,物理條件很單純:它的描述有沒有活著留在那份清單上。

那份清單有預算。官方文件寫得很明白,預算是模型 context window 的 1%,可以用 skillListingBudgetFraction 設定或 SLASH_COMMAND_TOOL_CHAR_BUDGET 環境變數改成固定字元數。單筆的 description 加上 when_to_use 合起來上限 1,536 個字元,超過就截斷,所以官方才會叫你把最關鍵的使用場景寫在最前面。

清單塞爆的時候會怎樣?它不會整個 skill 消失,而是開始砍描述,順序是從你最少叫的那些開始砍。名字一定留著,說明欄被清空。

拿現實世界的東西比:公司印一本內部服務目錄,規定只能印二十頁。服務太多印不下,就把冷門那幾項的「這個服務做什麼」整欄留白,只留名稱。名字還在,但看的人已經沒辦法從目錄判斷該不該找它。

這就是為什麼官方 troubleshooting 那節,「skill 沒被觸發」的第一條建議是去檢查 description 有沒有包含使用者真的會講出來的字。模型不笨,它手上那筆資料可能根本被截掉了。/doctor 會估算這份清單吃掉多少 context,也會告訴你最大的幾個貢獻者是誰。

我自己這台機器裝了近百個 skill,看到這段之後第一個反應是:那我那些冷門 skill 的描述,大概早就在清單裡變成一行光禿禿的名字了。

第二份:名字是從哪裡長出來的

使用者這條路完全不看描述。你打 / 加一個名字,Claude Code 做的是名字解析,跟模型那份清單是兩套獨立的東西。

而這個名字,來源比想像中複雜。官方文件列了五種情況:

skill 放在哪 指令名來自 例子
~/.claude/skills/.claude/skills/ 底下的目錄 目錄名 .claude/skills/deploy-staging//deploy-staging
巢狀的 .claude/skills/,且撞名 相對路徑 + 目錄名 apps/web/.claude/skills/deploy//apps/web:deploy
.claude/commands/ 底下的檔案 去掉副檔名的檔名 .claude/commands/deploy.md/deploy
plugin 的 skills/ 子目錄 frontmatter name 或目錄名,前面加 plugin 命名空間 my-plugin/skills/review//my-plugin:review
plugin 根目錄的 SKILL.md frontmatter name,沒寫就用 plugin 目錄名 name: review/my-plugin:review

注意第一列跟第四列的差別,這是最容易踩的坑。個人或專案的 skill,frontmatter 那個 name 只是顯示用的標籤,指令名依然來自目錄名;你改了 name 只會換掉選單上的標籤,要打的還是目錄名。plugin skill 剛好相反,name 會取代最後一段,plugin 前綴照樣留著。

一個現成的例子:plugin 名跟 skill 名撞在一起

claude-community 這個 marketplace 上有個叫 eli5 的 plugin,功能是把任何主題講成五歲小孩聽得懂的圖解。翻開它裝完之後的檔案:.claude-plugin/plugin.json"name": "eli5",skill 放在 skills/eli5/SKILL.md,那份 SKILL.md 的 frontmatter 也寫 name: eli5。三個地方都叫 eli5。

README 第六行教你這樣用:/eli5 how does DNS work。plugin.json 的 description 也寫 Use /eli5 <topic>

實際裝起來跑過一次,登記出來的指令是 /eli5:eli5。plugin 名跟 skill 名相同並不會被收斂成一個 eli5,命名空間該加還是加。

那 README 是錯的嗎?沒那麼簡單。官方文件另外寫了一句:在 plugin skill 裡,bare 的 /名字 也叫得動,除非那個名字已經被別的指令佔走了。所以 /eli5 是一個「順利的話也能用」的別名,正式名字仍然是帶命名空間的那個。這個別名通不通我沒有單獨驗證過。

寫 plugin 的人如果在 README 只教 bare 名字,等於把「你的環境剛好沒撞名」寫進了安裝說明。教 /plugin-name:skill-name 才是永遠成立的那個。

從 claude.ai 同步下來的 skill 規則更硬:名字跟任何其他指令撞到,同步的那個直接被跳過。比對時會忽略大小寫、空白與看不見的字元,全形字母也算同一個字,所以同步來的 Commit 沒辦法跟本機的 commit 並存。

拆完了,現在回頭看那兩個欄位

那兩個欄位在幹嘛,現在一句話講得完。

disable-model-invocation: true 動的是第一份,把這個 skill 從給模型的清單裡拿掉。描述不進 context,模型不知道它存在,自然不會自己去用。你打 /名字 照樣叫得動。預設是 false

user-invocable: false 動的是第二份,/ 選單裡看不到它,你直接打名字它也不會跑。但模型那邊完全不受影響,該用的時候照樣會用。預設是 true

官方把這三種組合列成一張表,我覺得這張表最值錢的是最右邊那欄:

frontmatter 你叫得動 Claude 叫得動 什麼時候進 context
(預設) 可以 可以 描述常駐 context,被叫時載入完整內容
disable-model-invocation: true 可以 不行 描述不進 context,你叫它時才載入完整內容
user-invocable: false 不行 可以 描述常駐 context,被叫時載入完整內容

第一列跟第三列的「進 context」欄位是一樣的。差別純粹在名字解析那邊有沒有被劃掉。

還有幾個連帶效果。disable-model-invocation: true 除了擋自動觸發,也會讓這個 skill 不被 preload 進 subagent;從 v2.1.196 開始,排程任務如果拿這個 skill 當 prompt,它也不會跑。這是一致的,設了這個欄位就代表「只有人喊才動」,排程不是人。

如果模型不死心硬要叫呢?Claude Code 會擋下來,而且會告訴它別想用別的方式把那些步驟重跑一遍。所以你大概會看到 Claude 回你一句「請你自己跑 /deploy」。

至於同時設 disable-model-invocation: trueuser-invocable: false,等於兩條路都封死,這個 skill 就變成誰都叫不動的擺飾。官方文件沒有寫這種組合會怎麼處理,是不是會有警告、還是就這樣安靜地存在,文件未載明

第三道閘門:叫得動之後,它能做什麼

前兩道管的是「誰能發動」,第三道管的是「發動之後手上有什麼工具」,這是 allowed-tools

這個欄位的名字很容易讓人誤會成白名單。它不是限制,是授權:列在裡面的工具,在叫起這個 skill 的那一則訊息裡不用再問你就能用。沒列的工具照樣叫得到,只是要照你原本的權限設定走一次流程。這個坑我在之前寫 Goose recipe 的那篇拆過,這裡不重複。

它的存活時間很短:授權在你送出下一則訊息時就清掉。skill 的正文會一直留在 context 裡(載進去之後整個 session 都在),但權限不會跟著留。要整個 session 都有效,得去 permission 設定加 allow rule,不是靠這個欄位。

反過來要拔掉工具的是 disallowed-tools,把工具從模型可用的池子裡移走,一樣下一則訊息就恢復。適合會跑很久的自動化 skill,例如不想讓它中途跳出來問你,就把 AskUserQuestion 拔掉。

有一件事該印在牆上:workspace trust 管不到 allowed-tools。專案裡的 skill 只要被叫起來授權就生效,包含你在從來沒信任過的資料夾裡跑 -p 的時候。skill 可以在 frontmatter 裡給自己開很大的權限,所以在別人的 repo 裡跑 Claude Code 之前,先翻一下 .claude/skills/ 底下的 allowed-tools 寫了什麼。

再往外一層還有 permission 規則可以管,語法是 Skill(名字) 精確比對、Skill(名字 *) 前綴比對,可以放 allow 也可以放 deny。想一次關掉全部,就在 /permissions 的 deny 裡加一行 Skill

不想改到那個檔案的時候

上面講的都要動 SKILL.md。但如果那個 skill 是別人 checked in 到共用 repo 的,你改了就會進 diff,這時候有 skillOverrides 這個設定可以從外面覆寫,寫在 .claude/settings.local.json

四種狀態:

模型看到 / 選單
"on" 名字+描述
"name-only" 只有名字
"user-invocable-only" 看不到
"off" 看不到 沒有

沒列進去的 skill 一律當成 "on"

"name-only" 這個狀態 frontmatter 做不到。回想第一節那個預算問題:清單塞爆的時候 Claude Code 會自己開始砍描述。"name-only" 等於你手動決定砍誰,把預算讓給真正需要模型自己判斷的那幾個 skill。

/skills 選單裡把游標移到某個 skill 上按空白鍵可以循環切換這四種狀態,按 Enter 存檔,不用自己手寫 JSON。選單裡 "user-invocable-only" 會顯示成 user-only

一個限制:plugin 的 skill 不吃 skillOverrides,那些要去 /plugin 管。

順便把欄位清單釘死

寫這篇的時候我特地回官方文件把 frontmatter 整張表數過一遍,因為列舉規格選項時人(跟模型)都會不自覺「補齊」出根本不存在的欄位。目前的完整清單就這 20 個,一個不多一個不少:

namedescriptionwhen_to_useargument-hintargumentsdisable-model-invocationuser-invocableallowed-toolsdisallowed-toolsmodeleffortcontextagentbackgroundhookspathsshellmetadatalicensecompatibility

除了 description 之外全部是選填的。布林欄位從 v2.1.218 開始除了 true / false,也吃 yesnoonoff10,大小寫都可以。

還有一個容易被忽略的分界:這 20 個是 Claude Code 的完整支援清單,但 Agent Skills 開放標準只認其中 6 個。上傳到 claude.ai、走 Skills API、或用 package_skill.py 打包的時候,只能留 namedescriptionlicensecompatibilitymetadataallowed-tools。多帶一個就直接報錯。它不會安靜地跳過那個欄位,整個打包會失敗:

1
2
Unexpected key(s) in SKILL.md frontmatter: argument-hint.
Allowed properties are: allowed-tools, compatibility, description, license, metadata, name

反過來說,只用這 6 個欄位寫出來的 skill,在 Claude Code 裡也能直接跑。要做跨工具共用的 skill,就照這 6 個欄位的範圍寫。

這個結構其實到處都是

回到開頭那個問題。你打指令跟模型自己判斷,走的確實是兩套機制:一套是有預算上限的描述清單,另一套是名字解析。frontmatter 那幾個欄位只是分別在這兩套裡劃掉某一行。

拆到這個層級才看得出來,這個結構不是 skill 專屬的。任何一個「東西很多、但介紹不能全塞進 context」的系統,最後都會長出同一個形狀,一份給機器判斷的索引(有預算、會被截斷、要寫得像使用者會說的話),一份給人查的名字表(要唯一、要能解決撞名)。MCP prompt、subagent、工具定義都是同一個問題。下次看到某個框架在講「自動觸發」,可以直接問它:那份給模型的索引長什麼樣、預算多少、爆掉的時候砍誰。

最後把我查完之後仍然不確定的東西攤開來,免得你照著我的推論去踩:

  • 同時設 disable-model-invocation: trueuser-invocable: false 會怎樣,文件未載明。
  • user-invocable: false 的 skill 被硬打 /名字 的時候,畫面上會出現什麼訊息,文件未載明,只寫了「不會跑」。
  • 清單超支被砍成只剩名字之後,模型光憑一個名字判斷該不該用它的效果如何,文件未載明。文件只保證名字一定在。
  • skillOverrides 的 key 對上巢狀 skill(那種指令名長成 apps/web:deploy 的)該怎麼寫,文件未載明。

想自己確認到底哪些 skill 正在佔你的 context,最快的一條是開一個 session 打 /context 看 Skills 那一列,再打 /doctor 看清單的估算成本跟前幾大貢獻者。看到數字之後,你大概會跟我一樣,回頭去把幾個一年沒叫過的 skill 設成 name-only

資料來源:Extend Claude with skills - Claude Code 官方文件