Here are claude code skills explained by what a session actually shows the model, rather than by what the docs promise: a skill is a SKILL.md in .claude/skills/<name>/, and the model chooses one from a listing that is capped at 8,000 characters. On this machine that listing carried 46 skills and only 15 of their descriptions. The other 31 — including the one written for this repository — arrived as a name and nothing else. Measured 2026-09-07 on Claude Code 2.1.263, reading every figure out of real session transcripts.
Key takeaways
- The skill listing has a character budget:
contextWindow × 4 × skillListingBudgetFraction, which is 8,000 characters by default. Ours needed 19,053. - Description slots are allocated by prior usage —
usageCount × 0.5^(days/7)— so a skill you have never run scores zero and is first out. - The same skill fired 0 of 5 times with only its name in the listing and 4 of 5 with its description, on identical prompts.
- A skill costs about two tokens per name until it runs. A 77,476-byte
SKILL.mdadded 6 prompt tokens while idle. - 13 of the 15 description slots went to skills that ship with Claude Code. Two went to the eleven installed by hand. None went to the twenty-one from a plugin.
The short answer
A skill is a folder with a SKILL.md in it. The frontmatter carries a name and a description; everything after the closing --- is the procedure. Skills and custom slash commands are one feature, which the earlier skills and slash commands piece covers, and the whole surface sits inside the complete Claude Code guide. Anthropic's agent skills documentation describes the format.
What no document says is that the model is not shown all of it. Reproduce it against your own machine:
npm run check:skills # → All 13 skill-listing guards passed. npm run check:skills -- --listing # → described : 15 (13 bundled, 2 the operator's own, 0 from the plugin) # → name-only : 31
Where the model meets a skill
Not in the system prompt. On the first user turn a session attaches a skill_listing block, and it is recorded verbatim in the transcript under ~/.claude/projects/, which is where Claude Code stores its history. Read it and you can see exactly what the model was given:
<system-reminder> The following skills are available for use with the Skill tool: - agents-sdk: Build AI agents on Cloudflare Workers using the Agents SDK. Load when… - cloudflare-email-service - cloudflare-one-migrations - durable-objects - shopify-plugin:shopify-admin - article-preflight - code-review: Review the current diff, or a PR number/branch/path target, for… </system-reminder>
Two shapes in one list. - name: description for fifteen of them, - name for the other thirty-one. The Skill tool's own description promises the first shape for all of them — "Available skills appear in a system-reminder listing with one-line descriptions" — and 31 of 46 rows have none.
The listing runs on a budget
The mechanism is in the binary, and it is arithmetic rather than a heuristic:
// defaults: contextWindow 200000, bytesPerToken 4, fraction 0.01 budget = Math.floor((contextWindow ?? 200000) * bytesPerToken * skillListingBudgetFraction) // = 200000 * 4 * 0.01 // = 8000 characters
When the full listing exceeds it, the tool logs a warning you will never see in normal use — "Skill listing over budget: N skills, X chars > 8000 budget — descriptions will be truncated" — and starts dropping descriptions. We confirmed it by moving the budget three ways:
| Budget | Bytes emitted | Descriptions |
|---|---|---|
| 2,000 | 7,074 | 11 of 46 |
| 8,000 (default) | 7,966 | 15 of 46 |
| 40,000 | 19,053 | 46 of 46 |
Two things fall out of that table. The full listing wants 19,053 characters, so 11,087 characters of description — 58% of it — are dropped by default. And at a 2,000-character budget the listing still emits 7,074: the names are a floor, and the budget only ever buys descriptions.
Who wins a description slot
Two rules, in order. Bundled commands are never truncated — /init, /code-review, /security-review and the rest keep their descriptions whatever else is on the shelf. Everything left competes for what remains, sorted by this:
// ~/.claude.json → skillUsage[name] = { usageCount, lastUsedAt }
score = usageCount * Math.max(0.5 ** (daysSinceLastUse / 7), 0.1)
// a skill with no entry scores 0
Prior usage, with a seven-day half-life and a floor at a tenth. Nothing about relevance to the current task, nothing about which directory the skill came from, nothing you can put in the file.
Here is what that produced on this machine, from 50 installed skills:
| Source | Installed | Got a description |
|---|---|---|
| Bundled with Claude Code | 18 | 13 |
| Installed by the operator | 11 | 2 |
| Arrived with a plugin | 21 | 0 |
Thirteen of the fifteen slots went to skills that shipped with the tool. Not one went to the twenty-one a plugin installed.
The cold start
Score zero, no description. No description, the model does not know what the skill is for. Not knowing, it does not choose it — so the usage count stays at zero.
We broke that loop once and watched what happened. A scratch skill, dv-procedure-alpha, sat in the listing as a bare name. We invoked it once by hand, typing /dv-procedure-alpha, and read the listing again:
before: - dv-procedure-alpha
after: - dv-procedure-alpha: Use whenever the user asks to record, log or note
a change in the project changelog, or asks what
shipped today.
One invocation is the whole difference — and the slot came out of another skill's pocket. cloudflare-one lost its description in the same turn dv-procedure-alpha gained one. The listing did not grow; it reallocated.
This is the finding that matters if you write skills. A skill has to be used before the model can find out what it is for. The bootstrap is manual, it is per-machine, and nothing in the file can shortcut it.
What a bare name costs
We built one skill and asked it five questions, twice. The skill's description: "Use whenever the user asks to record, log or note a change in the project changelog, or asks what shipped today." The prompts — "Record in the changelog that we fixed the date parser" and four like it — never name the skill. The listing state was read back out of each transcript rather than assumed, and the usage counter was reset between runs.
| Listing state | Fired | Runs |
|---|---|---|
| Name only | 0 | 5 |
| Description present | 4 | 5 |
Zero against four, same skill, same prompts, same model. The one miss in the described arm was "Log a note that the build now runs on Node 26", which never says "changelog".
That reproduces outside the lab. This repository has its own skill — .claude/skills/article-preflight/SKILL.md, which runs the publish gate — and it is one of the 31 bare names. We asked the same question twice, once at each budget:
| Listing state | Called the skill | Turns | Cost |
|---|---|---|---|
| Name only | no | 14 | $0.3565 |
| Description present | yes, first turn | 18 | $0.5045 |
The cheaper run is the worse one. With the description truncated away the session went and rebuilt the procedure by hand — Glob, Read, Grep, ToolSearch — and never touched the skill sitting in the repository it was working in.
A skill body is free until it runs
The other half of the design is the half that works. Only the listing row is ever in the prompt; the body loads on invocation. We put a 77,476-byte SKILL.md in a directory and measured both states:
| State | Prompt tokens | Turns | Cost |
|---|---|---|---|
| No skill in the directory | 25,300 | 1 | $0.0182 |
| The 77,476-byte skill, not invoked | 25,306 | 1 | $0.0182 |
| The same skill, invoked | 75,106 | 3 | $0.0581 |
Six tokens for a 77 KB file. And the reverse of the earlier finding: twenty skills carrying 100 KB of descriptions between them cost exactly what twenty skills carrying 4 KB cost — 37 tokens over baseline, about 1.85 tokens each — because in both cases only the names arrive.
Which makes one common piece of advice backwards. Trimming a skill's description to save context saves nothing; it only makes the skill harder to choose. The thing that costs is what the loaded body does to the window, and that is a bill you only pay when the skill actually runs.
Seven skills the model cannot choose
Seven names on this machine are registered, typeable as slash commands, and absent from the listing entirely: deep-research, design-sync, verify, debug, batch, doctor and run-skill-generator. This is not truncation — raising the budget to 40,000 does not bring them back.
They carry disable-model-invocation, and you can produce the same state yourself:
--- name: deploy-prod description: Deploy to production. Requires a human to type it. disable-model-invocation: true ---
That is the correct way to ship a dangerous procedure as a skill: the user can run /deploy-prod, and the model cannot decide to. It is a narrower guarantee than a PreToolUse hook's veto, because it stops the model reaching for this skill rather than stopping the underlying commands, but it is a real one and it is one frontmatter line.
What did not work
Our first conclusion was flatly wrong, and it looked like proof. We measured prompt tokens against 0, 20 and 20-with-huge-descriptions skills: 25,300, 25,337, 25,337. Twenty skills carrying 100 KB of descriptions moved the prompt by 37 tokens. The obvious reading is that descriptions are never sent at all — and it is exactly the signature you get when they are sent and then truncated away. Reading the skill_listing attachment out of the transcript is what corrected it, and it is the reason every number here comes from the transcript rather than the token counter.
The parser had the bug it was built to find. Our listing reader split each row on the first colon, which is fine for - code-review: Review the… and wrong for - shopify-plugin:shopify-admin: Write or explain…. Every plugin row was scored as having no description. It reported 25 of 46 described at a raised budget when the answer was 46 of 46 — a number that would have made the budget look like something other than a budget. Fixed by splitting on the first colon-space; every count above is from the fixed parser.
⚠️ This is one shelf. 50 skills, 11 of them the operator's own and 21 from a single plugin. The 15/31 split is a property of that population — install fewer skills and more descriptions fit. The budget, the priority formula and the cold start are properties of the tool.
⚠️ The budget formula is read out of the binary and confirmed at one context window. 200,000 → 8,000 characters is measured. The 40,000 characters it implies on a 1M-context model is arithmetic, not a measurement, and is not claimed as one.
⚠️ Five runs per state, one model. 0 of 5 against 4 of 5 is a direction, not a rate.
Best practices
- Invoke a new skill by hand once, on every machine that has it. That is what puts its description in front of the model. There is no file-level substitute.
- Raise
skillListingBudgetFractionif you own more than a handful of skills. At0.05you buy 40,000 characters for a cost measured in hundreds of tokens per turn. - Write the description as a trigger, not a summary. It is matched against what the user typed. "Use when the user asks to preflight or QA an article" beats "Editorial quality tooling".
- Do not trim a description to save context. It costs nothing until the budget is tight, at which point trimming it does not help — the whole line goes, not the excess.
- Put the length in the body, not the description. The body is free until it runs; the description competes for 8,000 shared characters.
- Use
disable-model-invocation: truefor anything destructive. One line, and the model can no longer choose it. - Check what your session actually shows. Run
/skills, or read theskill_listingattachment in the transcript. Assume nothing from the file.
Common mistakes
Assuming an installed skill is a discovered skill. Symptom: the skill works perfectly when typed and never fires on its own. Cause: it is a bare name in the listing. Fix: invoke it once, or raise the budget.
Renaming the skill by editing name:. The directory name is the name — a SKILL.md declaring name: totally-different-name registers as its folder. Fix: rename the directory.
Writing a 3,000-character description for reliability. It is cut to 1,536 in the listing, and the extra length makes the row more expensive to fit, so a long description is more likely to be dropped whole.
Expecting a skill to enforce a policy. A skill is chosen; it does not fire. For anything that must happen on every edit, a PostToolUse hook is the mechanism — and the hooks tutorial has the file shape.
Reading /skills too literally. Its own report says unused skills "add to the system prompt every turn" and, two lines later, that a dashed entry "is not in the current listing, costs nothing". Both are in the same output. The second one is what we measured.
Conclusion
Claude code skills explained in one sentence: a skill is a procedure the model may choose, and on a busy machine it will not get the chance, because its description lost an 8,000-character auction to skills that shipped with the tool. Write the skill, then do the two things that make it findable — invoke it once by hand, and raise skillListingBudgetFraction — and check the listing rather than the folder. If the job has to happen every time regardless, stop writing a skill and write a hook.
Frequently asked questions
What is a Claude Code skill?
Why is my Claude Code skill never triggered automatically?
How much context does a Claude Code skill use?
How do I make Claude Code show every skill description?
Does the SKILL.md name field set the skill's name?
Muhammad Kashif
Founder and editor of Devventa, covering AI coding assistants, Next.js and the modern AI development stack.




