Skills 機制:載入、觸發與執行

這一章只做一件事:把 skills 在 `src` 裡的實際執行機制,整理成可維護、可驗證的工程地圖。

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.tssrc/utils/plugins/loadPluginCommands.ts
展開 skill 內容注入回合 技能被呼叫(slash 或 SkillTool) 模型/使用者實際命中某個 skill 不一定,先是文本注入 src/utils/processUserInput/processSlashCommand.tsxsrc/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.tssrc/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 機制替代方案比較
方案 優點 缺點 為何目前未採用
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 高亮正確。
  • 確認本頁語言、術語與既有網站一致(台灣正體中文,避免外部推測性敘述)。
← 上一頁:指令系統 下一頁:工具系統 →