AIDive

Gói video

Bộ skill luôn kích hoạt: làm cho skill Claude Code tự load

9 phút đọc

Tóm tắt và kết luận

Một skill Claude Code có thể chạy hoàn hảo khi bạn gõ tên mà vẫn không bao giờ load khi bạn nhờ bằng lời thường. Trước khi quyết định load một skill, Claude đọc một dòng cho mỗi skill: name và description. Nó khớp dòng đó với lời của bạn. Nếu dòng đó không dùng từ của bạn, skill sẽ im lặng.

Trong lần chạy của chúng tôi, một skill release-notes có description "Generates changelog documents from repository history." đã load 0 of 5 phiên mới với yêu cầu "what did we ship this week?". Chúng tôi viết lại đúng dòng đó thành việc nó làm, khi nào dùng và những từ chúng tôi gõ. Cùng yêu cầu đó sau đó load 5 of 5.

Kết luận:

  • Skill đáng dùng cho một việc bạn lặp lại với kết quả cố định: ghi chú hằng tuần, mô tả pull request, checklist release.
  • Nó không đáng cho một câu hỏi dùng một lần, và nó không phải công cụ cho một quy tắc phải luôn đúng. Đó là việc của hook.
  • Một đống skill không ai dùng gây tốn kém, vì khi vượt ngân sách danh sách, Claude Code cắt description, cái ít dùng nhất trước.
  • Quy tắc duy nhất: viết description bằng chính những từ bạn gõ khi nhờ làm việc đó.

Bộ tài nguyên bên dưới có file skill của chúng tôi trước và sau, một mẫu description, checklist cho phiên mới và bài test hai mươi yêu cầu chúng tôi đã dùng.

Tài liệu và thực tế nói gì

Skill nằm ở đâu. Skill là một thư mục chứa SKILL.md, đặt trong ~/.claude/skills/<skill-name>/ cho mọi dự án hoặc trong .claude/skills/<skill-name>/ để team bạn cũng có. Một file name.md để lẻ sẽ không load. s1 s2

Claude đọc description, không đọc phần thân. Danh sách name và description nằm trong context ở mọi lượt, còn phần thân chỉ load khi skill được gọi. Tài liệu nói description là thứ Claude dùng để quyết định khi nào áp dụng skill. s1

Danh sách có giới hạn và có ngân sách. Phần description và when_to_use bị cắt cùng nhau ở 1.536 ký tự. Toàn bộ danh sách bị giới hạn ở 1% context window, và khi tràn, Claude Code bỏ description bắt đầu từ các skill bạn gọi ít nhất. Tài liệu nói điều này loại bỏ các từ khóa Claude cần để khớp yêu cầu của bạn. Bạn thấy cảnh báo trong debug log với --debug. s1

Phần đầu bị hỏng là cái bẫy trông như thành công. Nếu YAML sai định dạng, phần thân vẫn load với metadata rỗng, nên /skill-name vẫn chạy nhưng Claude không thể khớp description của bạn. Chạy với --debug để xem lỗi parse, hoặc chạy claude plugin validate .claude/skills. s1 s2

Kiểm tra nhãn. Nếu một skill xuất hiện trong /skills nhưng Claude không bao giờ gọi nó, thì hoặc disable-model-invocation: true đã được đặt, hoặc description không khớp với cách bạn diễn đạt yêu cầu. Nhãn user-only nghĩa là Claude sẽ không tự kích hoạt nó. s2

Một description tốt gồm những gì. Hướng dẫn của Anthropic nói description nên có cả việc skill làm và khi nào dùng, viết ở ngôi thứ ba, kèm các từ khóa mà người dùng gõ. Hướng dẫn cũng ghi rằng Claude dùng nó để chọn đúng skill trong số có thể hơn 100. s3

Claude có xu hướng kích hoạt thiếu. Skill-creator của Anthropic viết "currently Claude has a tendency to 'undertrigger' skills" và khuyên viết description hơi "mạnh tay". s18 Bài test trigger của nó dùng 20 truy vấn, gồm cả loại nên kích hoạt và không nên, trong đó near miss có giá trị nhất. Vòng lặp chia 60% để huấn luyện và 40% để test giữ lại, chạy mỗi truy vấn 3 lần, và giữ description có điểm test cao nhất. s4 Script đánh giá chạy từng truy vấn qua claude -p và kiểm tra skill có được dùng không. s19

Việc kích hoạt hoạt động giống khớp từ khóa. Trên binary thật, một bài test của bên thứ ba (22 prompt, Sonnet 4.5) đo được baseline 55% và 50% qua hai lần chạy, và một hook đánh giá bắt buộc đạt 100%. Prompt có token tường minh được kích hoạt gần 100% số lần, còn cách diễn đạt gián tiếp thì gần 0%. Nhận định của tác giả: gần với khớp từ khóa hơn là khớp ngữ nghĩa. Đây chỉ là một blog, một model cũ hơn và một repo khác, nên hãy coi là bằng chứng bổ trợ, không phải quy tắc. s5

Description là một trigger, không phải bản tóm tắt. Một cuộc kiểm toán chín skill thật đi đến cùng kết luận: description liệt kê các tình huống cụ thể tốt hơn description chỉ nói skill là gì. s8

Cũng có thất bại ngược lại. Một lập trình viên thấy skill kích hoạt sai việc, viết các eval trigger, viết lại description thành "Load when...", và ghi nhận rằng một skill đáng giữ thường sinh ra từ một thất bại bạn chán phải thấy hai lần. s11

Người khác cũng gặp. Issue "Claude Code not aware of skills in .claude/skills" có 75 lượt phản ứng và vẫn còn mở vào ngày 2026-10-17. Các cách xử lý tạm trong thread gồm một dòng chỉ dẫn trong CLAUDE.md và một hook. s7

Khi hook hơn skill. Câu trả lời hàng đầu trong một thread 778 upvote nói skill "basically just a prompt you save for later" mà Claude quyết định dùng hoặc bạn gọi bằng lệnh gạch chéo, còn hook dành cho lúc skill quá lỏng lẻo và bạn muốn tính xác định. s9 Một thread khác cảnh báo rằng các lời nhắc có thể phai dần trong một phiên dài. s10

Bộ tài nguyên

Skill trước và sau. Chỉ dòng description khác nhau, phần thân giống hệt.

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

Mẫu description, với description thật của chúng tôi làm ví dụ.

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

Checklist cho phiên mới.

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

Bộ test hai mươi yêu cầu: mười yêu cầu nên load skill, mười near miss không nên. Hãy sửa các truy vấn cho khớp với repo của bạn.

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

Script chạy từng yêu cầu trong một phiên claude -p mới riêng và đếm xem công cụ Skill có được gọi không.

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

Một script tổng hợp nhỏ cho một thư mục các lần chạy 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

Phương án dự phòng khi bắt buộc kích hoạt: một hook UserPromptSubmit nêu tên skill.

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

Làm vào thứ Hai

  • Viết ra cái prompt bạn gõ lại nhiều nhất tuần trước, và đúng kết quả bạn muốn từ nó.
  • Tạo .claude/skills/<name>/SKILL.md với name, description và một phần thân ngắn có đánh số.
  • Test bằng tên, rồi mở một phiên mới và nhờ việc đó bằng lời thường, không nhắc tên.
  • Nếu bị bỏ qua, hãy đọc /skills, claude --debug và claude plugin validate .claude/skills trước khi sửa bất cứ thứ gì.
  • Viết lại description bằng description-template.md: nó làm gì, khi nào dùng, những từ bạn gõ.
  • Test lại trong vài phiên mới, rồi một lần với skill đặt thành "off" trong skillOverrides và một lần bật lại.
  • Viết hai mươi yêu cầu của bạn, chạy bằng trigger_test.py, và chạy lại mỗi khi đổi description.
  • Với skill không được phép trượt, hãy quyết định: gọi bằng tên, hoặc thêm hook.

Chúng tôi đã đo được gì

Đây là các lần chạy demo nhỏ trên một bản sao tạm của repo mã nguồn mở Hono (s20), với Claude Code 2.1.294 và yêu cầu "what did we ship this week?" trên 8 conventional commit không phải merge kể từ thứ Hai 2026-10-12. Chúng không phải benchmark. Model, skill và repo của bạn sẽ làm số liệu khác đi.

Test Description Kết quả
Yêu cầu thường, 5 phiên mới Tóm tắt: "Generates changelog documents from repository history." Load 0 of 5
Yêu cầu thường, 5 phiên mới Viết lại: làm gì + khi nào + từ của chúng tôi Load 5 of 5
Gọi bằng tên (/release-notes) Tóm tắt Chạy 2 of 2
skillOverrides đặt off, rồi on Viết lại Off: bị bỏ qua. On: đã load
Test hai mươi yêu cầu, trước khi viết lại Tóm tắt 4 of 10 yêu cầu nên load đã load, 0 of 10 near miss, đạt 14 of 20
Test hai mươi yêu cầu, sau khi viết lại Viết lại 10 of 10 yêu cầu nên load, 0 of 10 near miss, đạt 20 of 20
Hook cộng description cũ Tóm tắt Load 1 of 1

Trước khi viết lại, 4 yêu cầu đã load là những yêu cầu chứa "release notes", "changelog", "notes" hoặc "release": đó chính là hiệu ứng từ khóa hiện ra rõ ràng. Sau khi viết lại, description rộng hơn không kéo vào một near miss nào.

Cảnh báo chúng tôi tìm thấy, và cách đọc trung thực. Với --debug, môi trường của chúng tôi in ra: "Skill listing over budget: 296 skills, 144833 chars > 30000 budget". Môi trường của chúng tôi mang quá nhiều skill, nên description bị cắt. Skill của dự án vẫn load ("Loaded 43 unique skills", một trong số đó là skill của dự án), và description cũ đã sống sót qua lần cắt: trong bài test hai mươi yêu cầu, yêu cầu chứa "changelog", một từ chỉ nằm trong description, đã load skill. Chúng tôi không chứng minh được ngân sách đối xử với một skill mới nói chung ra sao. Nếu bạn mang nhiều skill, hãy tự chạy phép kiểm tra. /skills trên môi trường của chúng tôi liệt kê 277 skill, và release-notes hiện là skill của dự án, đang bật, không có nhãn user-only.

Một lưu ý về bản thân công cụ: script đánh giá của Anthropic không thấy skill chút nào trên phiên bản của chúng tôi, nên chúng tôi chạy cùng các yêu cầu bằng script ngắn trong bộ tài nguyên. Phương pháp giống nhau, mỗi yêu cầu một phiên claude -p mới.

Đi xa hơn

Cũng hãy xử lý vấn đề ngược lại. Một description khớp quá rộng khiến skill kích hoạt ở chỗ không nên. Đó là lý do một nửa bộ test là near miss. Hãy thêm những từ bạn thật sự dùng, không phải mọi từ bạn nghĩ ra được. s11

Để bộ tối ưu đề xuất bản viết lại. Vòng lặp của skill-creator đề xuất các description mới và giữ cái tốt nhất theo điểm test giữ lại, để tránh quá khớp với các truy vấn bạn đã tinh chỉnh. Hãy dùng hai mươi yêu cầu của chính bạn làm bộ eval. s4

Dùng một prompt test đủ nội dung. Skill-creator ghi chú rằng Claude chỉ tham khảo skill cho những việc nó khó tự xử lý, nên một yêu cầu một bước tầm thường có thể không kích hoạt skill dù khớp hoàn hảo. s4

Vượt qua hook đơn giản. Bài test của bên thứ ba ở trên thấy rằng hook đánh giá bắt buộc, khiến Claude liệt kê từng skill là có hoặc không rồi mới kích hoạt, đạt 100% trong khi lời nhắc thụ động thì không, và hook type: "prompt" gốc hoạt động như không có hook. Hook của chúng tôi là bản đơn giản, và đó chỉ là một lần chạy demo. s5 Có báo cáo rằng các lời nhắc của hook có thể phai dần trong một phiên dài. s10

Giữ phần thân ngắn. Trong cuộc kiểm toán ở trên, skill hỏng duy nhất dài 1.002 dòng với MANDATORY khắp nơi, trong khi các skill khỏe đều ngắn. s8

Những gì chúng tôi bỏ qua. Chúng tôi không đề cập skill từ plugin và các bộ chấm điểm eval của plugin, vốn chỉ liên quan đến tác giả plugin, cũng như cách so sánh một skill với model trần trong nhiều tháng. Một số người cũng báo rằng skill viết cho model yếu hơn có thể trở thành gánh nặng, nên hãy test lại thỉnh thoảng. Chúng tôi chưa đo điều này.

Câu hỏi thường gặp

Tại sao /release-notes chạy được mà lời thường thì không? Gõ tên bỏ qua bước khớp. Với lời thường, Claude chỉ có name và một dòng description để khớp, nên một bản tóm tắt không chung từ nào với yêu cầu của bạn sẽ bị bỏ qua. s1

Tôi có phải khởi động lại Claude Code sau khi sửa skill không? Không. Claude Code nhận skill được thêm hoặc sửa trong ~/.claude/skills/ hoặc .claude/skills/ ngay trong phiên hiện tại. Nếu bạn tạo một thư mục skills cấp cao nhất chưa tồn tại lúc phiên bắt đầu, hãy chạy /reload-skills. s1

Description được dài bao nhiêu? Description và when_to_use bị cắt cùng nhau ở 1.536 ký tự trong danh sách, nên hãy đặt trường hợp dùng chính lên đầu. Hướng dẫn nền tảng của Anthropic quy định riêng giới hạn description là 1.024 ký tự cho skill tải lên bên ngoài Claude Code. s1 s3

Năm phiên mới có đủ để tin một bản sửa không? Không. Nó chỉ cho thấy hướng đi. Kết quả của chúng tôi là một demo trên một repo, đó là lý do bộ tài nguyên kết thúc bằng bài test hai mươi yêu cầu để bạn chạy lại mỗi khi đổi description.

Khi nào nên dùng hook thay vì skill? Khi một quy tắc phải đúng mọi lần, không chỉ khi Claude nhận ra có khớp. Hook thêm một dòng vào mỗi tin nhắn, nên chỉ để dành cho vài skill phải kích hoạt. s9

Tệp