Skip to content

AI CODING ASSISTANTS

Claude Code Skills Explained: 31 of 46 Arrive Blank

Claude code skills explained by measurement: a session showed the model 46 skills and only 15 of their descriptions. Yours is probably one of the 31 bare names.

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 usageusageCount × 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.md added 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:

Terminal
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:

skill_listing attachment, Claude Code 2.1.263
<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:

the budget, as the binary computes it
// 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:

BudgetBytes emittedDescriptions
2,0007,07411 of 46
8,000 (default)7,96615 of 46
40,00019,05346 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:

the priority score, as the binary computes it
// ~/.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:

SourceInstalledGot a description
Bundled with Claude Code1813
Installed by the operator112
Arrived with a plugin210

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 and after one invocation
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 stateFiredRuns
Name only05
Description present45

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 stateCalled the skillTurnsCost
Name onlyno14$0.3565
Description presentyes, first turn18$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:

StatePrompt tokensTurnsCost
No skill in the directory25,3001$0.0182
The 77,476-byte skill, not invoked25,3061$0.0182
The same skill, invoked75,1063$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:

.claude/skills/deploy-prod/SKILL.md
---
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 skillListingBudgetFraction if you own more than a handful of skills. At 0.05 you 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: true for anything destructive. One line, and the model can no longer choose it.
  • Check what your session actually shows. Run /skills, or read the skill_listing attachment 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?
A directory under .claude/skills/<name>/ containing a SKILL.md — YAML frontmatter with a name and description, then a body of instructions. The model sees the name and, budget permitting, the description; the body loads only when something invokes the skill. Skills and custom slash commands are the same feature, so every skill is also typeable as /<name>.
Why is my Claude Code skill never triggered automatically?
Most likely its description never reached the model. The skill listing is capped at 8,000 characters by default, and on a machine with 50 skills installed only 15 descriptions fit. The rest arrive as bare names. We measured the same skill at 0 of 5 firings with only its name in the listing and 4 of 5 with its description present.
How much context does a Claude Code skill use?
About two tokens per installed skill until something invokes it. We measured 20 skills carrying 100 KB of descriptions between them adding 37 prompt tokens over a directory with no skills at all, because only the names arrive. A 77,476-byte SKILL.md added 6 tokens idle and 49,800 when it ran.
How do I make Claude Code show every skill description?
Raise skillListingBudgetFraction in settings.json — it defaults to 0.01, meaning 1% of the context window measured in characters at four bytes per token, which is 8,000 characters on a 200k model. At 0.5 every description in our 46-row listing arrived in full, at 19,053 characters. The env var SLASH_COMMAND_TOOL_CHAR_BUDGET overrides it directly.
Does the SKILL.md name field set the skill's name?
No. The directory name is the name. We put name: totally-different-name in .claude/skills/dv-b/SKILL.md and the session registered it as dv-b. A name field with spaces and capitals was ignored the same way. Rename the directory to rename the skill.

Muhammad Kashif

Founder and editor of Devventa, covering AI coding assistants, Next.js and the modern AI development stack.