Кратко и вердикт
Скилл в Claude Code может отлично работать, когда вы набираете его имя, и при этом ни разу не загрузиться, когда вы просите обычными словами. Прежде чем решить, загружать ли скилл, Claude читает по одной строке на скилл: имя и описание. Он сопоставляет эту строку с вашими словами. Если в строке нет ваших слов, скилл молчит.
В нашем прогоне скилл release-notes с описанием "Generates changelog documents from repository history." загрузился в 0 из 5 свежих сессий на запрос "what did we ship this week?". Мы переписали эту одну строку так: что он делает, когда его использовать и какие слова мы набираем. После этого тот же запрос загрузил его в 5 из 5.
Вердикт:
- Скилл окупается на задаче, которую вы повторяете с фиксированным результатом: еженедельные заметки, описание pull request, чек-лист релиза.
- Он не окупается на разовом вопросе и не подходит для правила, которое должно выполняться всегда. Это работа хука.
- Куча скиллов, которыми никто не пользуется, обходится дорого: за пределами бюджета списка Claude Code обрезает описания, начиная с наименее используемых.
- Главное правило: пишите описание теми словами, которые вы реально набираете, когда просите об этой задаче.
В наборе ниже наш файл скилла до и после, шаблон описания, чек-лист для свежей сессии и тест из двадцати запросов, который мы использовали.
Что говорят документация и практика
Где живёт скилл. Скилл - это папка с SKILL.md внутри: либо в ~/.claude/skills/<skill-name>/ для всех проектов, либо в .claude/skills/<skill-name>/, чтобы он достался и вашей команде. Одиночный файл name.md не загружается. s1 s2
Claude читает описание, а не тело. Список имён и описаний находится в контексте на каждом ходу, а тело загружается только при вызове скилла. Документация говорит, что по описанию Claude решает, когда применять скилл. s1
У списка есть предел и бюджет. Текст description и when_to_use обрезается вместе на 1 536 символах. Весь список ограничен 1% окна контекста, и при переполнении Claude Code отбрасывает описания, начиная со скиллов, которые вы вызываете реже всего. Документация говорит, что при этом пропадают ключевые слова, нужные Claude для сопоставления с вашим запросом. Предупреждение вы увидите в debug-логе с --debug. s1
Сломанный заголовок - ловушка, похожая на успех. Если YAML составлен неправильно, тело загружается с пустыми метаданными, поэтому /skill-name всё ещё работает, но Claude не может сопоставить ваше описание. Запустите с --debug, чтобы увидеть ошибку разбора, или выполните claude plugin validate .claude/skills. s1 s2
Проверьте бейдж. Если скилл есть в /skills, но Claude его никогда не вызывает, значит, либо задано disable-model-invocation: true, либо описание не совпадает с тем, как вы формулируете запрос. Метка user-only означает, что Claude сам его не запустит. s2
Что входит в хорошее описание. В руководстве Anthropic сказано, что описание должно содержать и то, что скилл делает, и то, когда его использовать, быть написано в третьем лице и включать ключевые слова, которые набирают пользователи. Там же отмечено, что Claude выбирает по нему нужный скилл из потенциально 100+. s3
Claude склонен недозапускать. Skill-creator от Anthropic пишет: "currently Claude has a tendency to 'undertrigger' skills" и рекомендует чуть более "напористое" описание. s18 Его тест срабатывания использует 20 запросов, смесь тех, что должны запускать, и тех, что не должны, причём почти попадания самые ценные. Цикл делит их на 60% обучающих и 40% отложенных тестовых, прогоняет каждый запрос 3 раза и оставляет описание с лучшим результатом на тесте. s4 Скрипт оценки прогоняет каждый запрос через claude -p и проверяет, использовался ли скилл. s19
Активация похожа на сопоставление по ключевым словам. На реальном бинарнике один сторонний тест (22 промпта, Sonnet 4.5) показал базовый уровень 55% и 50% в двух прогонах, а хук принудительной оценки дал 100%. Промпт с явным токеном активировался почти в 100% случаев, а косвенная формулировка держалась около 0%. Вывод автора: это ближе к сопоставлению по ключевым словам, чем к семантическому. Это один блог, более старая модель и другой репозиторий, так что считайте это подтверждающим свидетельством, а не правилом. s5
Описание - это триггер, а не резюме. Аудит девяти реальных скиллов привёл к тому же выводу: описание, перечисляющее конкретные ситуации, лучше того, что говорит, чем скилл является. s8
Бывает и обратный сбой. Один разработчик видел, как скилл срабатывал на чужих задачах, написал оценки срабатывания, переписал описание как "Load when..." и отметил, что скилл, который стоит оставить, обычно рождается из сбоя, который вам надоело видеть дважды. s11
С этим столкнулись и другие. У issue "Claude Code not aware of skills in .claude/skills" 75 реакций, и 2026-10-17 оно всё ещё было открыто. Обходные пути в ветке: указатель в CLAUDE.md и хук. s7
Где хук лучше скилла. Лучший ответ в ветке на 778 голосов говорит, что скилл - это "basically just a prompt you save for later", который Claude решает применить или вы вызываете слеш-командой, а хук нужен, когда скиллы слишком расплывчаты и нужна детерминированность. s9 В другой ветке предупреждают, что напоминания-подсказки могут ослабевать в длинной сессии. s10
Набор
Скилл до и после. Отличается только строка описания, тело одинаковое.
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
Чек-лист для свежей сессии.
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
Тестовый набор из двадцати запросов: десять запросов, которые должны загрузить скилл, и десять почти попаданий, которые не должны. Поправьте запросы под свой репозиторий.
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}
]
Скрипт, который прогоняет каждый запрос в отдельной свежей сессии claude -p и считает, вызывался ли инструмент 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))
Небольшой подсчёт для папки прогонов 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")
Запасной вариант для обязательного срабатывания: хук UserPromptSubmit, который называет скилл по имени.
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.'"
}
]
}
]
}
}
Сделайте в понедельник
- Запишите промпт, который вы чаще всего набирали заново на прошлой неделе, и точный результат, который вы от него хотите.
- Создайте
.claude/skills/<name>/SKILL.mdс именем, описанием и коротким нумерованным телом. - Проверьте его по имени, затем откройте свежую сессию и попросите задачу обычными словами, без имени.
- Если его пропустили, прочитайте
/skills,claude --debugиclaude plugin validate .claude/skills, прежде чем что-либо править. - Перепишите описание по
description-template.md: что он делает, когда его использовать, какие слова вы набираете. - Повторите проверку в нескольких свежих сессиях, затем один раз со скиллом в положении
"off"вskillOverridesи один раз снова включённым. - Напишите свои двадцать запросов, прогоните их через
trigger_test.pyи повторяйте при каждом изменении описания. - Для скилла, который не должен промахиваться, решите: вызывать по имени или добавить хук.
Что мы измерили
Это небольшие демо-прогоны на отдельной копии open-source репозитория Hono (s20), с Claude Code 2.1.294 и запросом "what did we ship this week?" на 8 коммитах conventional commits без слияний с понедельника 2026-10-12. Это не бенчмарк. Ваша модель, ваши скиллы и ваш репозиторий сдвинут цифры.
| Тест | Описание | Результат |
|---|---|---|
| Обычный запрос, 5 свежих сессий | Сводка: "Generates changelog documents from repository history." | Загружен в 0 из 5 |
| Обычный запрос, 5 свежих сессий | Переписанное: что + когда + наши слова | Загружен в 5 из 5 |
Вызов по имени (/release-notes) |
Сводка | Запущен в 2 из 2 |
skillOverrides выключен, затем включён |
Переписанное | Выкл.: пропущен. Вкл.: загружен |
| Тест из двадцати запросов, до переписывания | Сводка | 4 из 10 запросов, которые должны загрузить, загрузили, 0 из 10 почти попаданий, пройдено 14 из 20 |
| Тест из двадцати запросов, после переписывания | Переписанное | 10 из 10 тех, что должны загрузить, 0 из 10 почти попаданий, пройдено 20 из 20 |
| Хук плюс старое описание | Сводка | Загружен в 1 из 1 |
До переписывания 4 запроса, которые загрузили скилл, содержали "release notes", "changelog", "notes" или "release": это эффект ключевых слов на виду. После переписывания более широкое описание не притянуло ни одного почти попадания.
Найденное предупреждение и честное прочтение. С --debug наше окружение вывело: "Skill listing over budget: 296 skills, 144833 chars > 30000 budget". В нашем окружении слишком много скиллов, поэтому описания обрезаются. Наш проектный скилл всё равно загрузился ("Loaded 43 unique skills", один из них проектный), и старое описание пережило обрезку: в тесте из двадцати запросов запрос со словом "changelog", которое есть только в описании, загрузил скилл. Мы не доказали, как бюджет обращается с новым скиллом в общем случае. Если у вас много скиллов, проверьте сами. /skills в нашем окружении показал 277 скиллов, а release-notes значился как проектный скилл, включён, без бейджа user-only.
Оговорка об инструменте: скрипт оценки от Anthropic вообще не увидел скилл на нашей версии, поэтому мы прогнали те же запросы коротким скриптом из набора. Метод тот же: одна свежая сессия claude -p на запрос.
Идём дальше
Исправьте и обратную проблему. Слишком широкое описание заставляет скилл срабатывать там, где не нужно. Поэтому половина тестового набора - почти попадания. Добавляйте слова, которые вы реально используете, а не все слова, которые придут в голову. s11
Пусть оптимизатор предложит переписывание. Цикл skill-creator предлагает новые описания и оставляет лучшее по результату на отложенном тесте, чтобы не переобучиться на запросах, под которые вы подстраивались. Используйте свои двадцать запросов как набор для оценки. s4
Берите содержательный тестовый промпт. Skill-creator отмечает, что Claude обращается к скиллам только для задач, с которыми не справится легко сам, поэтому тривиальный запрос в один шаг может не запустить скилл даже при идеальном совпадении. s4
Выйдите за рамки простого хука. Сторонний тест выше показал, что хук принудительной оценки, заставляющий Claude перечислить каждый скилл как да или нет и затем активировать, дошёл до 100% там, где пассивное напоминание не справилось, а нативный хук type: "prompt" вёл себя как отсутствие хука. Наш хук - простая версия, и это один демо-прогон. s5 Сообщают, что подсказки хуков могут ослабевать в длинной сессии. s10
Держите тело коротким. В упомянутом аудите единственный сломанный скилл был на 1 002 строки с MANDATORY повсюду, а здоровые были короткими. s8
Что мы оставили за рамками. Мы не касались скиллов плагинов и оценщиков плагинов, которые важны только авторам плагинов, и не сравнивали скилл с чистой моделью на протяжении месяцев. Некоторые также сообщают, что скиллы, написанные для более слабых моделей, могут превратиться в балласт, так что время от времени перепроверяйте их. Мы этого не измеряли.
FAQ
Почему /release-notes работает, а обычные слова нет?
Ввод имени обходит сопоставление. При обычных словах у Claude есть только имя и одна строка описания для сопоставления, поэтому сводка, не имеющая общих слов с вашим запросом, пропускается. s1
Нужно ли перезапускать Claude Code после правки скилла?
Нет. Claude Code подхватывает добавленный или изменённый скилл в ~/.claude/skills/ или .claude/skills/ в текущей сессии. Если вы создаёте каталог skills верхнего уровня, которого не было при старте сессии, выполните /reload-skills. s1
Какой длины может быть описание?
Description и when_to_use обрезаются вместе на 1 536 символах в списке, так что ставьте главный сценарий первым. Руководство платформы Anthropic отдельно ограничивает описание 1 024 символами для скиллов, загружаемых вне Claude Code. s1 s3
Хватит ли пяти свежих сессий, чтобы поверить исправлению? Нет. Они показывают направление. Наш результат - демо на одном репозитории, поэтому набор заканчивается тестом из двадцати запросов, который можно перезапускать при каждом изменении описания.
Когда использовать хук вместо скилла? Когда правило должно выполняться каждый раз, а не только когда Claude заметил совпадение. Хук добавляет строку в каждое сообщение, так что оставьте его для немногих скиллов, которые обязаны срабатывать. s9
AIDive