AIDive

影片資料包

必觸發 skill 資源包:讓 Claude Code skill 自己載入

閱讀約 9 分鐘

重點摘要與結論

Claude Code 的 skill 可以在你輸入名稱時完美執行,卻在你用平常的話要求時永遠不會載入。在 Claude 決定載入 skill 之前,它對每個 skill 只讀一行:名稱和描述。它會拿這一行去比對你說的話。如果這一行沒有用你的字,skill 就會保持沉默。

在我們的實測中,一個 release-notes skill 的描述是 "Generates changelog documents from repository history.",面對請求 "what did we ship this week?",在全新 session 裡 0 of 5 載入。我們把那一行改寫成它做什麼、何時使用,以及我們會打的字。同樣的請求接著就是 5 of 5 載入。

結論:

  • skill 值得用在你會重複、而且輸出固定的任務:每週說明、pull request 描述、release 檢查清單。
  • 一次性的問題不值得,而且它不是用來處理必須永遠成立的規則的工具。那是 hook 的工作。
  • 一大堆沒在用的 skill 會讓你付出代價,因為超過清單預算後,Claude Code 會裁掉描述,先裁最少使用的。
  • 那條規則:用你要求這項任務時真正會打的字,來寫描述。

下面的資源包有我們 skill 檔案的修改前後版本、描述範本、全新 session 檢查清單,以及我們使用的二十個請求測試。

文件與實際經驗怎麼說

skill 放在哪裡。 skill 是一個資料夾,裡面有一個 SKILL.md,可以放在 ~/.claude/skills/<skill-name>/(適用所有專案),或放在 .claude/skills/<skill-name>/(讓你的團隊也能用)。單獨的 name.md 檔案不會載入。 s1 s2

Claude 讀的是描述,不是本文。 名稱和描述的清單每一輪都在 context 裡,本文只有在 skill 被呼叫時才會載入。文件說,Claude 用描述來決定何時套用 skill。 s1

清單有上限,也有預算。 描述和 when_to_use 的文字合計在 1,536 個字元處被截斷。整個清單限制在 context window 的 1%,超出時,Claude Code 會從你最少呼叫的 skill 開始丟掉描述。文件說這會移除 Claude 用來比對你請求的關鍵字。你可以用 --debug 在 debug log 裡看到警告。 s1

標頭壞掉是看起來像成功的陷阱。 如果 YAML 格式錯誤,本文會帶著空的中繼資料載入,所以 /skill-name 仍然有效,但 Claude 無法比對你的描述。用 --debug 查看解析錯誤,或執行 claude plugin validate .claude/skills。 s1 s2

檢查標籤。 如果 skill 出現在 /skills 裡,但 Claude 從來不呼叫它,不是設了 disable-model-invocation: true,就是描述與你的說法不符。user-only 標籤表示 Claude 不會自己觸發它。 s2

好的描述包含什麼。 Anthropic 的指南說,描述應該同時包含 skill 做什麼和何時使用,用第三人稱撰寫,並帶有使用者會打的關鍵字。指南也提到,Claude 用它從可能上百個 skill 中選出對的那個。 s3

Claude 傾向於觸發不足。 Anthropic 的 skill-creator 寫道 "currently Claude has a tendency to 'undertrigger' skills",並建議描述稍微「強勢」一點。 s18 它的觸發測試用 20 個查詢,混合應觸發和不應觸發的,其中近似請求最有價值。這個迴圈以 60% 訓練、40% 保留測試來切分,每個查詢跑 3 次,並保留測試分數最高的描述。 s4 評估腳本會把每個查詢送進 claude -p,並檢查是否用到了 skill。 s19

啟動的行為像關鍵字比對。 在真實的執行檔上,一項第三方測試(22 個 prompt,Sonnet 4.5)在兩次執行中量到的基準線分別是 55% 和 50%,而強制評估的 hook 是 100%。帶有明確 token 的 prompt 幾乎 100% 都會啟動,而間接說法接近 0%。作者的解讀是:比較接近關鍵字比對,而不是語意比對。這只是一篇部落格、較舊的模型和不同的 repo,所以請當作佐證,不要當作規則。 s5

描述是觸發條件,不是摘要。 一份對九個真實 skill 的稽核得出了同樣的結論:列出具體情境的描述,勝過只說明 skill 是什麼的描述。 s8

相反的失敗也存在。 有位開發者看到 skill 在錯誤的任務上觸發,於是寫了觸發評估,把描述改寫成 "Load when...",並指出值得保留的 skill 通常誕生於一個你不想再看到第二次的失敗。 s11

其他人也碰到了。 「Claude Code not aware of skills in .claude/skills」這個 issue 有 75 個反應,到 2026-10-17 仍然開著。討論串裡的變通辦法包括 CLAUDE.md 指引和 hook。 s7

hook 勝過 skill 的地方。 一則 778 個讚的討論串裡,最高票的回答說,skill "basically just a prompt you save for later",由 Claude 決定使用,或由你用斜線指令呼叫;而 hook 則是在 skill 太鬆散、你想要確定性的時候用。 s9 另一個討論串則警告,提醒式的推動在長時間的 session 中可能會淡化。 s10

資源包

修改前後的 skill。只有描述那一行不同,本文完全一樣。

release-notes.before.SKILL.md

---
name: release-notes
description: Generates changelog documents from repository history.
---

# Release notes

1. Find the range: commits since last Monday (`git log --since="last monday" --no-merges --pretty="%h %s"`).
2. Drop `chore` and `docs` commits.
3. Group what is left by the conventional-commit scope (router, css, jsx, serve-static...), fixes and features apart.
4. Write one line per change, in plain English, present tense, no commit hashes, with the PR number at the end.
5. Output a markdown block titled "Release notes, week of <date>", with a two-line summary on top.

release-notes.before.SKILL.md on GitHub

release-notes.after.SKILL.md

---
name: release-notes
description: Writes this week's release notes from git log, grouped by scope (router, css, jsx). Use when the user asks what we shipped, what changed this week, for release notes, a changelog, or a summary of recent commits.
---

# Release notes

1. Find the range: commits since last Monday (`git log --since="last monday" --no-merges --pretty="%h %s"`).
2. Drop `chore` and `docs` commits.
3. Group what is left by the conventional-commit scope (router, css, jsx, serve-static...), fixes and features apart.
4. Write one line per change, in plain English, present tense, no commit hashes, with the PR number at the end.
5. Output a markdown block titled "Release notes, week of <date>", with a two-line summary on top.

release-notes.after.SKILL.md on GitHub

描述範本,以我們真實的描述作為示範範例。

description-template.md

# Skill description template

The description is the one line Claude reads before it decides to load your skill. Write it for the moment you ask, not for the file.

## Template

```
<Verb in third person> <what the skill produces>, <how it is shaped>. Use when the user asks <phrase 1>, <phrase 2>, <phrase 3>, or <phrase 4>.
```

Three parts, in this order:

1. What it does. Third person ("Writes", never "I can" or "You can use this"). Name the output.
2. When to use it. Start with "Use when the user asks".
3. Your words. The exact phrases you or your team type, copied from your own history, not synonyms you invented.

Put the key use case first. The description and `when_to_use` are cut together at 1,536 characters in the skill listing.

## Worked example (the real one from our run)

Before, a summary of the skill:

```
Generates changelog documents from repository history.
```

After, what + when + the words we type:

```
Writes this week's release notes from git log, grouped by scope (router, css, jsx). Use when the user asks what we shipped, what changed this week, for release notes, a changelog, or a summary of recent commits.
```

What changed:

- What: "Writes this week's release notes from git log, grouped by scope".
- When: "Use when the user asks ...".
- Your words: "what we shipped", "what changed this week", "release notes", "a changelog", "a summary of recent commits".

## Fill-in sheet

- Skill name:
- The prompt I keep retyping (copy it from your history):
- The output I want from it:
- Verb + output (what it does):
- Four phrases I really type (when):
- Phrases that sound close but are a different job (for your near misses):

## Check before you save

- Third person, no "I" and no "you".
- At least three phrases you really type.
- Under 1,536 characters in total.
- Not so wide that it matches a neighbour job. Add the near misses to your trigger test.

description-template.md on GitHub

全新 session 檢查清單。

trigger-checklist.md

# Fresh-session trigger checklist

A skill that runs by name can still be invisible to a plain request. Test it the way a teammate will use it.

## Test in a fresh session

1. Open a new session. A skill already used stays in the conversation, so a test in the same session proves nothing.
2. Ask for the task in your own words, without the skill's name. Example: "what did we ship this week?"
3. Watch for the Skill tool call before Claude touches anything else. If Claude runs the commands itself and writes its own summary, the skill was skipped.
4. Repeat in several fresh sessions. A handful of runs is a demo, not a statistic.

## If it was skipped, read why before you edit

1. `/skills`: is it listed, is it switched on, is there a `user-only` badge? A `user-only` badge means Claude will not trigger it on its own.
2. Ask "What skills are available?" and check your skill is in the answer.
3. `claude --debug`, then search the log for the skills line (how many loaded) and for a "Skill listing over budget" warning. Over budget means some descriptions are cut, the least invoked first.
4. `claude plugin validate .claude/skills`: a malformed header still lets `/name` work but leaves Claude with empty metadata to match against.
5. Check the file is `.claude/skills/<name>/SKILL.md`, not a loose `<name>.md`, and that the header starts on the first line.

## Fix, then prove it

1. Rewrite the description with `description-template.md`.
2. Fresh session, same plain request, several runs: does the Skill call come first now?
3. Baseline: set the skill to `"off"` in `skillOverrides` (saved to `.claude/settings.local.json` from `/skills`), ask again, then switch it back on.

## Hold it

1. Write twenty requests: ten that should load the skill, ten near misses (`evals.json` is our set).
2. Run each in its own fresh session and note whether the skill loaded (`trigger_test.py`, `tally.py`).
3. Re-run the list every time you change the description.
4. For a skill that must never miss: call it by name, or add a UserPromptSubmit hook (`hook.json`).

trigger-checklist.md on GitHub

二十個請求的測試集:十個應該載入 skill 的請求,十個不應載入的近似請求。請依你自己的 repo 修改查詢內容。

evals.json

[
  {"query": "what did we ship this week?", "should_trigger": true},
  {"query": "can you put together the release notes for this week", "should_trigger": true},
  {"query": "what changed in the repo since monday", "should_trigger": true},
  {"query": "write the changelog for the commits we merged this week", "should_trigger": true},
  {"query": "I need a summary of this week's commits for the team update on friday", "should_trigger": true},
  {"query": "draft the weekly notes of what got fixed, grouped by area", "should_trigger": true},
  {"query": "list what landed this week, router and css changes apart", "should_trigger": true},
  {"query": "give me something to post in the team channel about this week's fixes", "should_trigger": true},
  {"query": "recap the work merged since last monday", "should_trigger": true},
  {"query": "what's new in hono this week? short version for the release post", "should_trigger": true},
  {"query": "explain what commit 08807ce1 changes in the trie router", "should_trigger": false},
  {"query": "the jsx tests are failing on cloneElement, can you fix them", "should_trigger": false},
  {"query": "write a commit message for my staged changes", "should_trigger": false},
  {"query": "who wrote most of the serve-static code?", "should_trigger": false},
  {"query": "add a CHANGELOG entry format section to CONTRIBUTING.md", "should_trigger": false},
  {"query": "show me the diff between main and my branch", "should_trigger": false},
  {"query": "bump the version in package.json to the next patch", "should_trigger": false},
  {"query": "why does the css helper minify url() like that?", "should_trigger": false},
  {"query": "squash my last three commits into one", "should_trigger": false},
  {"query": "review this PR for breaking changes before I merge it", "should_trigger": false}
]

evals.json on GitHub

這個腳本會在各自獨立的全新 claude -p session 裡執行每個請求,並計算 Skill 工具有沒有被呼叫。

trigger_test.py

"""Twenty-prompt trigger test for one skill, the method skill-creator describes, run on the real skill folder.
For each query: a fresh `claude -p` in the repo; the run counts as loaded if the Skill tool is called with the skill's name
before the run ends (stopped early as soon as it is). Usage:
  python3 trigger_test.py <repo> <skill-name> <SKILL.md to install> <evals.json> <out.json> [workers]"""
import json, os, shutil, subprocess, sys
from concurrent.futures import ThreadPoolExecutor

repo, name, skill_md, evals_path, out = sys.argv[1:6]
workers = int(sys.argv[6]) if len(sys.argv) > 6 else 4
dest = os.path.join(repo, '.claude', 'skills', name)
os.makedirs(dest, exist_ok=True)
shutil.copy(skill_md, os.path.join(dest, 'SKILL.md'))
evals = json.load(open(evals_path))
allowed = ['Bash(rtk git log *)', 'Bash(git log *)', 'Bash(git show *)', 'Bash(date *)', 'Skill', 'Read']

def run(item):
    cmd = ['claude', '-p', item['query'], '--output-format', 'stream-json', '--verbose', '--allowedTools', *allowed]
    p = subprocess.Popen(cmd, cwd=repo, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
    loaded = False
    try:
        for line in p.stdout:
            try:
                d = json.loads(line)
            except ValueError:
                continue
            if d.get('type') == 'assistant':
                for c in d['message'].get('content', []):
                    if c.get('type') == 'tool_use' and c['name'] == 'Skill' and name in json.dumps(c.get('input', {})):
                        loaded = True
            if loaded or d.get('type') == 'result':
                break
    finally:
        if p.poll() is None:
            p.kill()
    return {**item, 'loaded': loaded, 'pass': loaded == item['should_trigger']}

with ThreadPoolExecutor(workers) as ex:
    results = list(ex.map(run, evals))
should = [r for r in results if r['should_trigger']]
shouldnt = [r for r in results if not r['should_trigger']]
summary = {'loaded_when_it_should': sum(r['loaded'] for r in should), 'of_should': len(should),
           'loaded_when_it_should_not': sum(r['loaded'] for r in shouldnt), 'of_should_not': len(shouldnt),
           'passed': sum(r['pass'] for r in results), 'total': len(results)}
json.dump({'skill': name, 'skill_md': skill_md, 'summary': summary, 'results': results}, open(out, 'w'), indent=2)
for r in results:
    print(f"[{'PASS' if r['pass'] else 'FAIL'}] {'loaded ' if r['loaded'] else 'skipped'} expected={'load' if r['should_trigger'] else 'skip'}: {r['query']}")
print(json.dumps(summary))

trigger_test.py on GitHub

一個小型統計腳本,用來彙整一個資料夾裡的 claude -p --output-format stream-json 執行結果。

tally.py

"""Count whether a `claude -p --output-format stream-json` run loaded a given skill. Usage: python3 tally.py <skill> run1.jsonl [run2.jsonl ...]"""
import json, sys
skill, files = sys.argv[1], sys.argv[2:]
fired = 0
for f in files:
    hit = False
    for line in open(f):
        try:
            d = json.loads(line)
        except ValueError:
            continue
        if d.get('type') == 'assistant':
            for c in d['message'].get('content', []):
                if c.get('type') == 'tool_use' and c['name'] == 'Skill' and skill in json.dumps(c.get('input', {})):
                    hit = True
    fired += hit
    print(f"{f}: {'loaded' if hit else 'skipped'}")
print(f"{skill}: loaded in {fired} of {len(files)} runs")

tally.py on GitHub

必須觸發時的備案:一個點名這個 skill 的 UserPromptSubmit hook。

hook.json

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo 'If the user asks about release notes, a changelog or what shipped this week, use the release-notes skill.'"
          }
        ]
      }
    ]
  }
}

hook.json on GitHub

週一就動手做

  • 寫下你上週重打最多次的 prompt,以及你想要它產出的確切輸出。
  • 建立 .claude/skills/<name>/SKILL.md,包含名稱、描述和簡短的編號本文。
  • 先用名稱測試,再開一個全新 session,不帶名稱、用平常的話要求這項任務。
  • 如果被跳過,在改任何東西之前,先讀 /skills、claude --debug 和 claude plugin validate .claude/skills。
  • 用 description-template.md 改寫描述:它做什麼、何時使用、你會打的字。
  • 在幾個全新 session 裡重測,再把 skillOverrides 裡的 skill 設為 "off" 測一次,然後再開回來測一次。
  • 寫下你的二十個請求,用 trigger_test.py 執行,並在每次修改描述時重跑。
  • 對於絕不能漏掉的 skill,做個決定:用名稱呼叫,或加上 hook。

我們量到的結果

這些是在開源 Hono repo(s20)的暫存複本上做的小型示範執行,使用 Claude Code 2.1.294,請求是 "what did we ship this week?",對象是 2026-10-12 週一以來的 8 個非 merge 的 conventional commit。它們不是基準測試。你的模型、你的 skill 和你的 repo 都會讓數字不同。

測試 描述 結果
平常的請求,5 個全新 session 摘要式:"Generates changelog documents from repository history." 載入 0 of 5
平常的請求,5 個全新 session 改寫後:做什麼 + 何時 + 我們的字 載入 5 of 5
用名稱呼叫(/release-notes) 摘要式 執行 2 of 2
skillOverrides 設為 off,再設為 on 改寫後 Off:被跳過。On:已載入
二十個請求的測試,改寫前 摘要式 應載入的 4 of 10 載入,近似請求 0 of 10,通過 14 of 20
二十個請求的測試,改寫後 改寫後 應載入 10 of 10,近似請求 0 of 10,通過 20 of 20
hook 加上舊的描述 摘要式 載入 1 of 1

改寫之前,載入的那 4 個請求,正是包含 "release notes"、"changelog"、"notes" 或 "release" 的請求:這就是明擺在眼前的關鍵字效應。改寫之後,涵蓋範圍變廣的描述沒有拉進任何一個近似請求。

我們發現的警告,以及誠實的解讀。 用 --debug 時,我們的環境印出:"Skill listing over budget: 296 skills, 144833 chars > 30000 budget"。我們的環境裝了太多 skill,所以描述會被截斷。我們的專案 skill 仍然載入了("Loaded 43 unique skills",其中一個是專案 skill),而且舊的描述撐過了裁切:在二十個請求的測試裡,包含 "changelog" 這個只存在於描述裡的字的請求,載入了 skill。我們沒有證明預算對一般的新 skill 如何處理。如果你裝了很多 skill,請自己做這項檢查。我們的環境上 /skills 列出了 277 個 skill,release-notes 顯示為專案 skill,已開啟,沒有 user-only 標籤。

關於工具本身的一點說明:Anthropic 的評估腳本在我們的版本上完全看不到這個 skill,所以我們用資源包裡那段簡短的腳本跑了同樣的請求。方法是一樣的,每個請求用一個全新的 claude -p session。

延伸閱讀

也要處理相反的問題。 描述涵蓋太廣,會讓 skill 在不該觸發的地方觸發。這就是為什麼測試集有一半是近似請求。加進你真正會用的字,而不是你想得到的每一個字。 s11

讓最佳化器提出改寫。 skill-creator 的迴圈會提出新的描述,並依保留測試的分數留下最好的那個,避免過度擬合你調校用的查詢。把你自己的二十個請求當作評估集。 s4

使用有份量的測試 prompt。 skill-creator 指出,Claude 只會在任務是它自己不容易處理的時候才參考 skill,所以一個單步驟的瑣碎請求,即使描述完全吻合,也可能不會觸發 skill。 s4

超越單純的 hook。 上面那項第三方測試發現,強制評估的 hook(讓 Claude 對每個 skill 逐一回答是或否,然後啟動)達到 100%,而被動提醒做不到;原生的 type: "prompt" hook 的表現則和沒有 hook 一樣。我們的 hook 是簡單版,而且只是一次示範執行。 s5 有報告指出,hook 的提醒在長時間的 session 中可能會淡化。 s10

保持本文簡短。 在上面的稽核裡,唯一壞掉的 skill 有 1,002 行,到處都是 MANDATORY,而健康的 skill 都很短。 s8

我們略過的部分。 我們沒有涵蓋外掛 skill 和外掛評估評分器(那只與外掛作者有關),也沒有涵蓋如何在數個月內拿 skill 和原生模型做比較。也有人回報,為較弱模型寫的 skill 可能會變成累贅,所以請不時重新測試。我們沒有量測這一點。

常見問題

為什麼 /release-notes 有效,平常的話卻沒用? 輸入名稱會繞過比對。用平常的話時,Claude 只有名稱和一行描述可以比對,所以和你的請求沒有共同字的摘要式描述就會被跳過。 s1

編輯 skill 之後需要重新啟動 Claude Code 嗎? 不需要。Claude Code 會在目前的 session 裡直接讀到 ~/.claude/skills/ 或 .claude/skills/ 下新增或修改的 skill。如果你建立了一個 session 啟動時不存在的頂層 skills 目錄,請執行 /reload-skills。 s1

描述可以多長? 清單中,描述和 when_to_use 合計在 1,536 個字元處被截斷,所以要把關鍵的使用情境放在最前面。Anthropic 的平台指南另外規定,在 Claude Code 之外上傳的 skill,描述上限是 1,024 個字元。 s1 s3

五個全新 session 足以信任一個修正嗎? 不夠。它只顯示方向。我們的結果是在一個 repo 上的示範,所以資源包最後才附上二十個請求的測試,讓你每次修改描述時都能重跑。

什麼時候該用 hook 而不是 skill? 當某條規則必須每次都成立,而不只是在 Claude 注意到符合時才成立。hook 會為每則訊息多加一行,所以只留給少數必須觸發的 skill。 s9

檔案