AIDive

Paket video

Kit skill-yang-aktif: bikin skill Claude Code ter-load sendiri

9 menit baca

TL;DR dan vonis

Skill Claude Code bisa jalan sempurna saat kamu mengetik namanya dan tetap tidak pernah ter-load saat kamu memintanya dengan kata biasa. Sebelum memutuskan me-load sebuah skill, Claude membaca satu baris per skill: name dan description. Ia mencocokkan baris itu dengan kata-katamu. Kalau baris itu tidak memakai kata-katamu, skill tetap diam.

Dalam run kami, skill release-notes dengan description "Generates changelog documents from repository history." ter-load di 0 dari 5 sesi baru pada permintaan "what did we ship this week?". Kami menulis ulang satu baris itu menjadi apa yang dikerjakan, kapan dipakai, dan kata yang kami ketik. Permintaan yang sama lalu me-load-nya di 5 dari 5.

Vonisnya:

  • Skill berguna untuk tugas yang kamu ulang dengan output tetap: catatan mingguan, deskripsi pull request, checklist rilis.
  • Tidak untuk pertanyaan sekali pakai, dan bukan alat untuk aturan yang harus selalu berlaku. Itu tugas hook.
  • Tumpukan skill yang tak dipakai merugikan, karena melewati budget daftar, Claude Code memotong description, yang paling jarang dipakai lebih dulu.
  • Satu aturannya: tulis description dengan kata yang benar-benar kamu ketik saat meminta tugas itu.

Kit di bawah berisi file skill kami sebelum dan sesudah, template description, checklist sesi baru, dan tes dua puluh permintaan yang kami pakai.

Apa kata docs dan lapangan

Di mana skill berada. Skill adalah folder dengan SKILL.md di dalamnya, entah di ~/.claude/skills/<skill-name>/ untuk semua proyek atau di .claude/skills/<skill-name>/ agar timmu juga mendapatkannya. File name.md yang tergeletak tidak ter-load. s1 s2

Claude membaca description, bukan body. Daftar name dan description ada di context setiap giliran, dan body baru ter-load saat skill dipanggil. Docs menyebut description adalah yang dipakai Claude untuk memutuskan kapan menerapkan skill. s1

Daftar punya batas dan budget. Teks description dan when_to_use dipotong bersama pada 1.536 karakter. Seluruh daftar dibatasi 1% dari context window, dan saat meluap, Claude Code membuang description mulai dari skill yang paling jarang kamu panggil. Docs menyebut ini menghilangkan keyword yang dibutuhkan Claude untuk mencocokkan permintaanmu. Kamu melihat peringatan di debug log dengan --debug. s1

Header yang rusak adalah jebakan yang tampak seperti sukses. Jika YAML rusak, body ter-load dengan metadata kosong, sehingga /skill-name tetap jalan tetapi Claude tak bisa mencocokkan description-mu. Jalankan dengan --debug untuk melihat error parse, atau jalankan claude plugin validate .claude/skills. s1 s2

Periksa badge-nya. Jika skill muncul di /skills tetapi Claude tak pernah memanggilnya, berarti disable-model-invocation: true aktif atau description tidak cocok dengan cara kamu meminta. Label user-only berarti Claude tidak akan memicunya sendiri. s2

Isi description yang baik. Panduan Anthropic menyebut description harus memuat apa yang dikerjakan skill dan kapan dipakai, ditulis dalam sudut pandang orang ketiga, dengan istilah kunci yang diketik pengguna. Panduan itu juga mencatat Claude memakainya untuk memilih skill yang tepat dari kemungkinan 100+. s3

Claude cenderung undertrigger. Skill-creator Anthropic berkata "currently Claude has a tendency to 'undertrigger' skills" dan menyarankan description yang sedikit "pushy". s18 Tes triggernya memakai 20 query, campuran should-trigger dan should-not-trigger, dengan near miss yang paling berharga. Loop-nya membagi 60% train dan 40% test held-out, menjalankan tiap query 3 kali, dan mempertahankan description dengan skor test terbaik. s4 Skrip eval menjalankan tiap query lewat claude -p dan memeriksa apakah skill dipakai. s19

Aktivasi berperilaku seperti pencocokan keyword. Pada binary asli, satu tes pihak ketiga (22 prompt, Sonnet 4.5) mengukur baseline 55% dan 50% di dua run, dan hook forced-evaluation 100%. Prompt dengan token eksplisit teraktivasi hampir 100% waktu, sementara frasa tidak langsung mendekati 0%. Pembacaan penulisnya: lebih mirip pencocokan keyword daripada pencocokan semantik. Ini satu blog, model lebih lama, dan repo berbeda, jadi anggap sebagai bukti pendukung, bukan aturan. s5

Description adalah pemicu, bukan ringkasan. Audit atas sembilan skill nyata sampai pada kesimpulan yang sama: description yang mendaftar situasi konkret mengalahkan yang menyebut skill itu apa. s8

Kegagalan sebaliknya juga ada. Seorang developer melihat skill terpicu pada tugas yang salah, menulis eval trigger, menulis ulang description menjadi "Load when...", dan mencatat bahwa skill yang layak dipertahankan biasanya lahir dari kegagalan yang bosan kamu lihat dua kali. s11

Orang lain juga mengalaminya. Issue "Claude Code not aware of skills in .claude/skills" punya 75 reaksi dan masih terbuka pada 2026-10-17. Workaround di thread-nya mencakup petunjuk di CLAUDE.md dan sebuah hook. s7

Di mana hook mengalahkan skill. Jawaban teratas di thread dengan 778 upvote berkata skill itu "basically just a prompt you save for later" yang diputuskan Claude untuk dipakai atau kamu panggil dengan slash command, dan hook dipakai saat skill terlalu longgar dan kamu ingin kepastian. s9 Thread lain memperingatkan bahwa dorongan pengingat bisa memudar di sesi panjang. s10

Kit

Skill sebelum dan sesudah. Hanya baris description yang berbeda, body-nya identik.

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

Template description, dengan description asli kami sebagai contoh kerja.

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 sesi baru.

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

Set tes dua puluh permintaan: sepuluh permintaan yang seharusnya me-load skill, sepuluh near miss yang tidak boleh. Ubah query-nya agar cocok dengan repo-mu.

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

Skrip yang menjalankan tiap permintaan di sesi claude -p barunya sendiri dan menghitung apakah tool Skill dipanggil.

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

Rekap kecil untuk folder berisi run 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

Cadangan wajib-aktif: hook UserPromptSubmit yang menyebut nama 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

Lakukan ini hari Senin

  • Tulis prompt yang paling sering kamu ketik ulang minggu lalu, dan output persis yang kamu inginkan darinya.
  • Buat .claude/skills/<name>/SKILL.md dengan name, description, dan body bernomor yang singkat.
  • Tes lewat nama, lalu buka sesi baru dan minta tugasnya dengan kata biasa tanpa menyebut nama.
  • Jika dilewati, baca /skills, claude --debug, dan claude plugin validate .claude/skills sebelum mengedit apa pun.
  • Tulis ulang description dengan description-template.md: apa yang dikerjakan, kapan, dan kata yang kamu ketik.
  • Tes ulang di beberapa sesi baru, lalu sekali dengan skill diatur "off" di skillOverrides dan sekali lagi saat dihidupkan kembali.
  • Tulis dua puluh permintaanmu, jalankan dengan trigger_test.py, dan jalankan ulang setiap kali description berubah.
  • Untuk skill yang tak boleh meleset, putuskan: panggil lewat nama, atau tambahkan hook.

Yang kami ukur

Ini run demo kecil di klon scratch repo open source Hono (s20), dengan Claude Code 2.1.294 dan permintaan "what did we ship this week?" pada 8 commit conventional non-merge sejak Senin 2026-10-12. Ini bukan benchmark. Model, skill, dan repo-mu akan menggeser angkanya.

Tes Description Hasil
Permintaan biasa, 5 sesi baru Ringkasan: "Generates changelog documents from repository history." Ter-load di 0 dari 5
Permintaan biasa, 5 sesi baru Ditulis ulang: apa + kapan + kata kami Ter-load di 5 dari 5
Dipanggil lewat nama (/release-notes) Ringkasan Jalan di 2 dari 2
skillOverrides off, lalu on Ditulis ulang Off: dilewati. On: ter-load
Tes dua puluh permintaan, sebelum penulisan ulang Ringkasan 4 dari 10 permintaan should-load ter-load, 0 dari 10 near miss, 14 dari 20 lulus
Tes dua puluh permintaan, sesudah penulisan ulang Ditulis ulang 10 dari 10 should-load, 0 dari 10 near miss, 20 dari 20 lulus
Hook plus description lama Ringkasan Ter-load di 1 dari 1

Sebelum penulisan ulang, 4 permintaan yang ter-load adalah yang memuat "release notes", "changelog", "notes" atau "release": efek keyword terlihat jelas. Sesudahnya, description yang lebih luas tidak menarik satu pun near miss.

Peringatan yang kami temukan, dan pembacaan jujurnya. Dengan --debug, setup kami mencetak: "Skill listing over budget: 296 skills, 144833 chars > 30000 budget". Setup kami membawa terlalu banyak skill, sehingga description dipotong. Skill proyek kami tetap ter-load ("Loaded 43 unique skills", salah satunya skill proyek), dan description lama selamat dari pemotongan: di tes dua puluh permintaan, permintaan yang memuat "changelog", kata yang hanya ada di description, me-load skill. Kami tidak membuktikan bagaimana budget memperlakukan skill baru secara umum. Jika kamu membawa banyak skill, jalankan pengecekannya sendiri. /skills di setup kami mendaftar 277 skill, dan release-notes tampil sebagai skill proyek, aktif, tanpa badge user-only.

Catatan tentang alatnya sendiri: skrip eval Anthropic tidak melihat skill sama sekali di versi kami, jadi kami menjalankan permintaan yang sama dengan skrip pendek di kit. Metodenya sama, satu sesi claude -p baru per permintaan.

Lanjutkan lebih jauh

Perbaiki juga masalah sebaliknya. Description yang cocok dengan terlalu banyak hal membuat skill aktif di tempat yang tidak semestinya. Karena itu separuh set tes adalah near miss. Tambahkan kata yang benar-benar kamu pakai, bukan semua kata yang terpikir. s11

Biarkan optimizer mengusulkan penulisan ulang. Loop skill-creator mengusulkan description baru dan mempertahankan yang terbaik menurut skor test held-out, agar tidak overfit pada query yang kamu setel. Pakai dua puluh permintaanmu sendiri sebagai set eval. s4

Pakai prompt tes yang substansial. Skill-creator mencatat Claude hanya merujuk skill untuk tugas yang tak mudah ia tangani sendiri, jadi permintaan sepele satu langkah mungkin tidak memicu skill meski cocok sempurna. s4

Melangkah lebih jauh dari hook biasa. Tes pihak ketiga di atas menemukan bahwa hook forced-evaluation, yang membuat Claude mendaftar tiap skill sebagai ya atau tidak lalu mengaktifkan, mencapai 100% di mana pengingat pasif tidak, dan bahwa hook native type: "prompt" berperilaku seperti tanpa hook. Hook kami versi sederhana, dan ini satu run demo. s5 Ada laporan bahwa dorongan hook bisa memudar di sesi panjang. s10

Jaga body tetap singkat. Dalam audit di atas, satu-satunya skill yang rusak panjangnya 1.002 baris dengan MANDATORY di mana-mana, sedangkan yang sehat pendek. s8

Yang kami lewatkan. Kami tidak membahas skill plugin dan grader eval plugin, yang hanya penting bagi penulis plugin, atau cara membandingkan skill dengan model polos selama berbulan-bulan. Sebagian orang juga melaporkan skill yang ditulis untuk model lebih lemah bisa menjadi beban mati, jadi tes ulang sesekali. Itu tidak kami ukur.

FAQ

Kenapa /release-notes jalan tetapi kata biasa tidak? Mengetik nama melewati pencocokan. Dengan kata biasa, Claude hanya punya name dan satu baris description untuk dicocokkan, jadi ringkasan yang tidak berbagi kata dengan permintaanmu dilewati. s1

Apakah saya harus me-restart Claude Code setelah mengedit skill? Tidak. Claude Code mengambil skill yang ditambah atau diedit di bawah ~/.claude/skills/ atau .claude/skills/ di sesi saat ini. Jika kamu membuat direktori skills tingkat atas yang belum ada saat sesi dimulai, jalankan /reload-skills. s1

Seberapa panjang description boleh? Description dan when_to_use dipotong bersama pada 1.536 karakter di daftar, jadi taruh kasus pemakaian utama di depan. Panduan platform Anthropic secara terpisah membatasi description 1.024 karakter untuk skill yang diunggah di luar Claude Code. s1 s3

Apakah lima sesi baru cukup untuk mempercayai perbaikan? Tidak. Itu menunjukkan arah. Hasil kami adalah demo di satu repo, itulah sebabnya kit berakhir dengan tes dua puluh permintaan yang bisa kamu jalankan ulang setiap kali mengubah description.

Kapan sebaiknya memakai hook, bukan skill? Saat sebuah aturan harus berlaku setiap kali, bukan hanya saat Claude menemukan kecocokan. Hook menambah satu baris di tiap pesan, jadi simpan untuk beberapa skill yang harus aktif. s9

File