90 秒總覽
Claude Code 的 skills 不是單一來源。`src/commands.ts` 會把 bundled skills、`.claude/skills`、plugin skills、legacy commands-as-skills 與其他 command 來源整合為同一個 registry,再由 `getSkillToolCommands()` 和 `getSlashCommandToolSkills()` 分別提供給模型側與使用者側流程。實際觸發有兩條主路:使用者 `/skill` 走 `processSlashCommand.tsx`,模型主動使用 skill 走 `SkillTool.ts`。兩條路最後都會把 skill 內容注入回合上下文,並套用 allowed tools、model、effort、permission 與 compaction 保留機制。
執行路徑(主骨架)
// src/commands.ts
export async function getCommands(cwd: string): Promise<Command[]> {
const allCommands = await loadAllCommands(cwd)
const dynamicSkills = getDynamicSkills()
// availability + isEnabled 過濾
return ...
}
export const getSkillToolCommands = memoize(async (cwd: string) => ...)
export const getSlashCommandToolSkills = memoize(async (cwd: string) => ...)
重點:skills 先進同一份 command registry,再由不同入口做二次過濾,而不是維護兩套獨立清單。
啟動/刷新:載入 skills 來源(bundled + skill dirs + plugin + legacy)
合併進 commands registry(含 dynamic/conditional skills)
每回合附件:注入 skill listing 與必要 discovery 信號
使用者路徑:
/xxx -> processSlashCommand -> getMessagesForPromptSlashCommand
-> 注入 SKILL 內容 + command permissions attachment -> 查詢主回合
模型路徑:
SkillTool(skill,args) -> validate + permission
-> inline 或 fork 執行
-> 注入 skill 內容/結果並更新 context modifier
flowchart TD
A[載入 skills 來源] --> B[getCommands 合併 registry]
B --> C{觸發來源}
C -->|使用者 /skill| D[processSlashCommand.tsx]
C -->|模型 SkillTool| E[SkillTool.ts]
D --> F[getMessagesForPromptSlashCommand]
E --> G{inline 或 fork}
F --> H[注入 skill 內容與權限]
G --> H
H --> I[query loop 執行]
I --> J[compaction 保留與後續恢復]
路徑判讀重點
- Skill listing 與 skill discovery 是兩層機制;listing 不是完整 discovery 的替代品(`src/utils/attachments.ts`、`src/query.ts`)。
- SkillTool 只接受 prompt-based skill,且會先做 `validateInput` 與 `checkPermissions`(`src/tools/SkillTool/SkillTool.ts`)。
- `context: fork` 的 skill 會交給子 agent 跑,不與主回合同 token 預算(`executeForkedSkill`)。
- skill 內容會進 `addInvokedSkill(...)`,是 compaction 後可恢復的依據(`src/utils/processUserInput/processSlashCommand.tsx`)。
觸發判斷與時機(回合引擎視角)
在回合引擎中,skills 的「使用時機」不是硬編碼 if/else,而是由三層訊號組合出來:可見性(listing)、相關性提示(discovery)、以及模型最終是否呼叫 `SkillTool`。這三層對應到不同模組,不能混成同一件事。
Turn 開始
-> getAttachmentMessages: 注入 skill_listing(可用技能清單)
-> query.ts: startSkillDiscoveryPrefetch(背景預抓技能相關線索)
-> 模型先執行一般推理與工具呼叫
-> query.ts: collectSkillDiscoveryPrefetch(把 discovery 線索注入)
-> 模型判斷是否呼叫 SkillTool
若呼叫: SkillTool.validate -> checkPermissions -> call(inline/fork)
若不呼叫: 繼續一般工具路徑
sequenceDiagram participant U as User participant Q as query.ts participant A as attachments.ts participant M as Model participant S as SkillTool U->>Q: submit message Q->>A: getAttachmentMessages A-->>Q: skill_listing Q->>Q: startSkillDiscoveryPrefetch Q->>M: send context + tools M-->>Q: tool calls / reasoning Q->>Q: collectSkillDiscoveryPrefetch Q->>M: inject skill_discovery (if any) M->>S: call SkillTool? (optional) S-->>M: inline/fork result M-->>Q: final response
| 層級 | 作用 | 來源(src) | 是否代表「已執行 skill」 |
|---|---|---|---|
| skill listing | 告訴模型目前有哪些 skills 可用 | src/utils/attachments.ts#getSkillListingAttachments |
否,只是可見性 |
| skill discovery | 提供與當前任務可能相關的技能線索 | src/query.ts + src/utils/attachments.ts |
否,只是提示 |
| SkillTool call | 真正載入並執行 skill | src/tools/SkillTool/SkillTool.ts |
是,這才是執行點 |
實務判讀規則
- 看到 `skill_listing` 不代表模型一定會用 skill;它只表示「可選項存在」。
- 看到 `skill_discovery` 也不代表一定會執行 skill;模型可能判斷一般工具更直接。
- 只有出現 `SkillTool` 呼叫,才算進入 skills 執行流程(含 `validateInput/checkPermissions/call`)。
- 使用者輸入 `/skill-name` 屬於 slash command 入口,不是模型自主觸發。
讀文件 vs 執行 Script:觸發階段對照
這裡要先拆開三件事:系統讀 `SKILL.md`、skill 內容被展開注入、模型後續呼叫工具去讀檔或執行命令。它們是不同階段,不要混在一起判斷。
| 事件 | 觸發階段 | 觸發條件 | 是否會執行命令 | 來源(src) |
|---|---|---|---|---|
| 讀取 `SKILL.md` 並解析 frontmatter | 技能載入/發現 | 掃描 skills 目錄、plugin skills 或動態技能發現時 | 不會,只是讀檔建索引 | src/skills/loadSkillsDir.ts、src/utils/plugins/loadPluginCommands.ts |
| 展開 skill 內容注入回合 | 技能被呼叫(slash 或 SkillTool) | 模型/使用者實際命中某個 skill | 不一定,先是文本注入 | src/utils/processUserInput/processSlashCommand.tsx、src/tools/SkillTool/SkillTool.ts |
| 執行 skill 內 shell 注入(`!`) | skill 展開過程 | 該 skill 內容含 shell 注入語法,且非 MCP skill | 會,透過 shell 執行 | src/skills/loadSkillsDir.ts#executeShellCommandsInPrompt |
| 讀專案文件(Read/Grep) | 模型後續工具呼叫 | skill 內容或當前任務讓模型決定去讀檔 | 會呼叫工具,但不等於 shell script | src/query.ts(回合工具迴圈) |
| Remote/MCP skill 內容注入 | 技能被呼叫 | 命中 remote canonical skill 或 MCP skill 路徑 | 預設不走本地 shell 注入 | src/tools/SkillTool/SkillTool.ts、src/skills/loadSkillsDir.ts |
場景案例(判讀模板)
案例 A:只看到 skill listing
輸入情境:一般提問,系統注入了 `skill_listing`。
回合順序:listing 出現,但模型沒有呼叫 SkillTool。
是否執行 script:不會。
原因:listing 是可見性訊號,不是執行指令。
案例 B:使用者直接輸入 `/某個 skill`
輸入情境:使用者明確走 slash command 入口。
回合順序:`processSlashCommand` 展開 skill 內容,注入回合。
是否執行 script:只有 skill 內容包含 shell 注入語法時才會執行。
原因:展開 skill 不等於一定執行 shell;要看 skill 文本是否含可執行片段。
案例 C:skill 文字寫「先讀 docs 再修」
輸入情境:skill 內容要求先讀文件。
回合順序:先展開 skill,再由模型呼叫 Read/Grep。
是否執行 script:通常不會;這是工具讀檔,不是 shell 注入。
原因:文件讀取發生在後續工具階段,不是 skill 載入/展開當下。
案例 D:MCP skill 或 remote skill
輸入情境:模型命中 MCP skill 或 remote canonical skill。
回合順序:內容被注入,但不走本地 shell 注入路徑。
是否執行 script:預設不會由本地 shell 注入觸發。
原因:MCP/remote 路徑有額外安全邊界,避免把遠端 skill 文本直接當本地 shell 執行。
關鍵決策(為什麼這樣設計)
決策 1:skills 併入統一 command registry
原因:`loadAllCommands()` 先整合,再由不同函式過濾,能保證匹配規則與可見性判斷一致,減少雙軌清單漂移。
代價:過濾規則較複雜,維護者需同時理解 `getCommands`、`getSkillToolCommands`、`getSlashCommandToolSkills` 的差異。
決策 2:skills 支援 inline / fork 兩種執行上下文
原因:簡單技能可 inline 降低開銷;重型或需隔離的技能用 fork,避免汙染主回合狀態與 token 預算。
代價:需維護兩條執行路徑的一致性,包含 telemetry、權限與結果回傳格式。
決策 3:動態與條件技能採漸進啟用
原因:只在檔案操作命中路徑時發現或啟用(`discoverSkillDirsForPaths`、`paths` frontmatter),降低啟動負擔與噪音。
代價:技能可見性會受上下文影響,除錯時要先確認是否已被 discover/activate。
決策 4:回合中以訊號驅動,而非強制規則驅動
原因:讓模型可根據當前任務選擇 skill 或一般工具,避免「每回合都硬套 skill」造成過度流程化。
代價:可預測性較低;要透過 listing/discovery/telemetry 來提升可觀測性,而不是靠單一硬規則。
決策 5:把 shell 注入限制在特定 skill 來源
原因:本地 skill 與 MCP/remote skill 的信任邊界不同,不能同樣對待。
代價:不同來源技能在「可執行能力」上行為不完全一致,文件需明確標注。
替代方案與取捨
| 方案 | 優點 | 缺點 | 為何目前未採用 |
|---|---|---|---|
| skills 與 commands 完全分離 registry | 概念隔離明確,文件看起來更直覺 | 匹配與權限規則容易分岔,重複邏輯增加 | 目前設計偏重一致性與復用,統一 registry 更穩 |
| 所有 skills 一律 inline | 流程單純、實作面少 | 重型技能會拖慢主回合,隔離性不足 | 已有 `context: fork` 需求,不能只用單一路徑 |
| 每回合都完整列出全部 skills | 可見性最高,不怕漏 | token 成本高,且會放大無關技能噪音 | 現行改用 listing 去重 + discovery prefetch 平衡成本 |
失敗路徑與防護
Failure 1:模型呼叫不存在或不可調用 skill
症狀:SkillTool 收到未知 skill、非 prompt skill、或 `disableModelInvocation` skill。
防護:`validateInput` 直接拒絕並回傳明確 error code 與訊息(`src/tools/SkillTool/SkillTool.ts`)。
Failure 2:skill 內容觸發不必要 discovery,導致高延遲
症狀:大型 SKILL.md 被當成一般輸入再做 discovery,回合變慢。
防護:在 `getMessagesForPromptSlashCommand` 走 `getAttachmentMessages` 時設定 `skipSkillDiscovery: true`(`src/utils/processUserInput/processSlashCommand.tsx`)。
Failure 3:動態技能已載入但清單快取未刷新
症狀:新 discover 的技能未即時出現在可用列表。
防護:動態載入完成觸發 signal 清快取(`onDynamicSkillsLoaded` + command memoization 清理鏈)。
Failure 4:把 listing/discovery 誤當成「skill 已執行」
症狀:觀察到附件中有 skills 訊號,就誤判功能一定走 skill 路徑,導致診斷方向錯誤。
防護:以 `SkillTool` 實際呼叫紀錄作為執行判據;附件訊號只當作候選與提示,不當作執行證據。
Failure 5:把「讀文件」誤判成「已執行 script」
症狀:看到模型用了 Read/Grep,就以為 skill 內 shell 片段也已經執行。
防護:分開看兩類事件:工具讀檔事件 vs shell 注入執行事件,兩者要各自驗證。
實作驗證(你改完要怎麼確認)
- 確認本頁敘述可對應到以下程式碼:`src/commands.ts`、`src/skills/loadSkillsDir.ts`、`src/utils/plugins/loadPluginCommands.ts`、`src/utils/processUserInput/processSlashCommand.tsx`、`src/tools/SkillTool/SkillTool.ts`、`src/utils/attachments.ts`、`src/query.ts`。
- 確認使用者路徑:`/skills` 相關敘述與 slash command 流程一致,未把 SkillTool 與 slash 流程混為一談。
- 確認模型路徑:有清楚描述 validate/permission/context(inline,fork) 的先後順序。
- 確認時機路徑:有明確區分 listing、discovery、SkillTool call 三層,且每層都有對應 src 位置。
- 確認讀/執行路徑:有清楚區分「讀 SKILL.md」「展開 skill」「Read/Grep 讀文件」「shell script 執行」。
- 確認案例段落:至少 4 個場景,且每個都有「是否執行 script」的明確判斷。
- 確認導覽可從首頁、群組頁與側欄進入本頁,且 active page 高亮正確。
- 確認本頁語言、術語與既有網站一致(台灣正體中文,避免外部推測性敘述)。