The 480 Characters That Decide Whether Your Skill Ever Runs
Most tutorials on writing a Claude skill teach you the file format. Create a directory, add a SKILL.md, write some frontmatter, done.
That part takes ten minutes. Then you ship it, and it never fires. Claude keeps doing the thing you wrote the skill to stop it doing, and you quietly go back to pasting the same instructions into chat.
The file format is not the hard part. The hard part is that a skill competes for a scarce resource — context — and the only thing standing between your 400 lines of carefully written procedure and total irrelevance is a couple hundred characters of description text that most people write last, in thirty seconds, as an afterthought.
I built a real skill to work through this: blog-factcheck, which enforces the factual-grounding rules this site already runs on. Every number traceable to a primary source, qualifiers preserved, examples attributed. It is 373 lines across four files. Exactly 480 characters of it decide whether it ever runs.
Here is what I learned building it.
Why Most Skills Never Fire
Start with what Claude actually knows about your skill before it decides to use it.
At session startup, Claude does not load your skill. It loads a list of skill descriptions — just enough to know what exists. In Claude Code's own context window breakdown, that listing is budgeted at roughly 450 tokens in a typical setup, with this note attached:
One-line descriptions of available skills so Claude knows what it can invoke. Full skill content loads only when Claude actually uses one.
So at the moment of decision, Claude has not read your procedure. It has not seen your careful edge-case handling or your worked examples. It has one or two sentences, sitting in a list next to every other skill you have installed, and it is pattern-matching your request against that list.
This reframes the whole exercise. You are not writing a document for Claude to follow. You are writing two things with completely different jobs:
- A description whose only job is to win a matching contest at startup.
- A body that Claude reads only after the description already won.
Most skills fail at step one and the body never gets a chance. A skill named code-review described as "Reviews code" will lose to the model's own judgment every time, because "reviews code" describes roughly everything Claude already does.
There is a second trap here. Once a skill does load, the docs are explicit about what happens:
When you or Claude invoke a skill, the rendered
SKILL.mdcontent enters the conversation as a single message and stays there across later turns.
It stays. Every line you wrote is now a recurring cost on every subsequent turn of that conversation. A bloated skill does not just waste tokens once — it crowds out the actual work for the rest of the session.
The Context Math Nobody Shows You
This is where skills differ from CLAUDE.md, and it is the distinction that actually matters.
CLAUDE.md loads at startup, every session, whether or not it is relevant. That is correct for facts you always need — build commands, conventions, project layout. It is a bad place for a twelve-step procedure you run once a month, because you pay for those twelve steps in every unrelated session all month.
A skill inverts that. The docs put it plainly:
Unlike CLAUDE.md content, a skill's body loads only when it's used, so long reference material costs almost nothing until you need it.
So the rule is less "skills are better" and more a question about frequency and shape:
CLAUDE.md | Skill | |
|---|---|---|
| Loads | Every session, always | Only when invoked |
| Cost when unused | Full token cost | Description only |
| Right for | Facts: "the build command is npm run dev" | Procedures: "audit this PRD against the template" |
| Wrong for | Multi-step procedures used occasionally | Facts needed in every single session |
The official guidance for when to make the jump is a good tell:
Create a skill when you keep pasting the same instructions, checklist, or multi-step procedure into chat, or when a section of CLAUDE.md has grown into a procedure rather than a fact.
That second clause described my situation exactly. This repo's CLAUDE.md had a "Factual Grounding" section that had quietly grown from a rule into a procedure — six bullets about source tiers, qualifier preservation, and attribution that I was paying for in every session, including the ones where I was fixing CSS.
One more wrinkle worth knowing, because it bit me. The skill listing behaves differently from the rest of startup context during compaction:
Unlike the rest of the startup content, this listing is not re-injected after
/compact. Only skills you actually invoked get preserved.
In a long session, skills you never used stop being discoverable after a compaction. If a skill matters to a long-running workflow, invoke it early rather than hoping it gets picked up on hour three.
Anatomy of a Real Skill
Here is the actual frontmatter I shipped, not a toy:
---
name: blog-factcheck
description: Fact-check a blog post or PRD draft before publishing. Verifies
every number, metric, and technical claim against a primary source, preserves
qualifiers like "up to", flags unattributed examples, and reports a publish
verdict. Read-only - reports findings, does not edit the draft.
when_to_use: Use when the user says fact-check, verify claims, check sources,
"is this accurate", or is about to publish or commit a draft under
public/resources/. Also use before adding a new post to BLOG_POSTS.
argument-hint: [path-to-markdown]
allowed-tools: Read, Grep, Glob, WebFetch, WebSearch,
Bash(python3 ${CLAUDE_SKILL_DIR}/scripts/extract_claims.py *)
---
Five fields, each earning its place:
name is optional — it defaults to the directory name. I set it anyway, because the directory name and the / menu entry drifting apart is a small annoyance that compounds.
description is the one field that genuinely matters. More on it below.
when_to_use is the field most people miss. It appends to description and is the natural home for trigger phrases — the literal words someone types right before they want this skill. Separating them keeps the description readable while still packing in matcher surface area.
argument-hint shows up during autocomplete. Pure ergonomics, costs nothing.
allowed-tools pre-approves tools for the turn that invokes the skill. The docs are precise about the blast radius:
Tools Claude can use without asking permission during the turn that invokes this skill. The grant clears when you send your next message.
Note what is not in that list: Write and Edit. This skill reports; it does not rewrite my drafts. That is a deliberate product decision encoded in the frontmatter rather than in prose that the model might reason its way around. If you are writing a skill that touches anything destructive, the inverse field matters too — disable-model-invocation: true prevents Claude from auto-firing it, so a /deploy skill only ever runs when a human types it.
The ${CLAUDE_SKILL_DIR} variable resolves to the directory holding SKILL.md, which is how the skill finds its own bundled script regardless of the current working directory. It works in two places specifically:
Claude Code substitutes
${CLAUDE_SKILL_DIR}and${CLAUDE_PROJECT_DIR}in two places: the skill's markdown content, and Bash rules in theallowed-toolsfrontmatter.
Both places are used above — once in the permission rule, once in the body's bash command.
Progressive Disclosure in Four Tiers
The official body guidance is a hard number:
Keep
SKILL.mdunder 500 lines. Move detailed reference material to separate files.
Paired with the reason:
Keep the body itself concise. Once a skill loads, its content stays in context across turns, so every line is a recurring token cost. State what to do rather than narrating how or why.
"State what to do rather than narrating how or why" is the line I'd tattoo on a new skill author. In my experience most first drafts are mostly justification. Claude does not need to be persuaded that fact-checking is important. It needs to know the steps.
Here is how blog-factcheck's 373 lines distribute across four distinct loading tiers:
| Tier | Content | Size | When it enters context |
|---|---|---|---|
| 1 | description + when_to_use | 480 chars | Every session, at startup |
| 2 | SKILL.md body | 70 lines | On invocation |
| 3 | references/*.md | 97 lines | Only when the body sends Claude to read them |
| 4 | scripts/extract_claims.py | 206 lines | Never |
Tier 4 is the one worth staring at. That script is 55% of the skill by line count, and its source code never enters context at all. Claude runs it and reads the output. A deterministic text-processing routine does not need to be understood by a language model to be used by one.
The split between tiers 2 and 3 is a judgment call that comes down to one question: does Claude need this to execute the happy path, or only to handle a specific case? The procedure lives in SKILL.md. The 56-line source-tier taxonomy and the 41-line output spec live in references/, pulled in by a single line of the body:
Read `references/source-tiers.md` and assign a tier.
That one line costs about a dozen tokens. The 56 lines behind it cost nothing until a claim actually needs tiering.
Four Tiers of Loading
blog-factcheck is 373 lines. 70 load when it runs. 480 characters load when it doesn't.
Tier 1 is the only always-on cost. At startup Claude sees descriptions only — budgeted at roughly 450 tokens for the whole listing in a typical setup. Your skill is competing inside that line item.
Tier 2 is recurring, not one-time. Once invoked, SKILL.md stays in context across later turns. Every line is a cost you pay again on each turn.
Tier 4 is 55% of the skill and costs nothing. The script's source never enters context. Claude runs it and reads the output — deterministic work does not need to be understood by a model to be used by one.
Push the Deterministic Work into a Script
This is the lesson I'd most want to pass on, and it is a product instinct more than a technical one: do not spend model judgment on work that has a right answer.
Finding every number in a markdown file is not a judgment call. It is a regex. Deciding whether arXiv paper §5.2 actually supports the sentence you wrote is entirely a judgment call. Mixing the two means paying model attention for the first and diluting it for the second.
So blog-factcheck opens with a deterministic pass:
python3 ${CLAUDE_SKILL_DIR}/scripts/extract_claims.py <path> --only-unsourced
The script finds quantitative claims, absolutes, and attributed statements, then reports which ones lack a citation in the same block. Run against my Alpamayo post, real output:
Scanned: public/resources/blogs/nvidia-alpamayo-autonomous-driving.md
Claims found: 16
Missing a nearby source: 16
VERIFY THESE FIRST (no citation in the same block)
----------------------------------------------------
L19 [quantitative] A diffusion-based action expert converts everything into
64 waypoints — a precise 6.4-second trajectory at 10 Hz
values: 10 Hz, 6.4
L65 [quantitative] - Up to 12% improvement in planning accuracy on challenging
edge cases vs. trajectory-only approaches - 35% reduction in close
encounter rate during closed-loop simulation...
values: 12%, 35%, 45%, 37%, 99ms
qualifiers to preserve: up to
That last line is the whole point. The script does not know what a planning benchmark is. It knows the draft said "up to 12%", and that if the final text says "12% improvement" with the hedge stripped, a factual error has been introduced. It hands Claude a short, specific list of things to verify instead of a 1,300-word draft and a vague instruction to be careful.
The body then tells Claude what the script cannot decide:
Open the primary source, not coverage of it. A press release about a paper is not the paper. Confirm the source states this number, in this context. Never record a citation you did not open.
Division of labor: the script finds candidates, the model exercises judgment, the references hold the taxonomy. Each in the tier where it is cheapest.
Writing a Description That Actually Triggers
Back to the 480 characters.
The combined budget is generous and the ceiling is documented:
Put the key use case first: the combined
descriptionandwhen_to_usetext is truncated at 1,536 characters in the skill listing to reduce context usage.
I used 480 of 1,536. Not because brevity is a virtue here — this is the one place in a skill where length is nearly free — but because everything past the point of unambiguous matching is noise.
What makes a description win its matching contest:
Lead with the triggering situation, not the capability. "Fact-check a blog post or PRD draft before publishing" names a moment. "Verifies factual claims" names a capability. Claude is matching against what the user just said, and users describe moments.
Name the artifacts. My description contains public/resources/, BLOG_POSTS, PRD. These are strings that appear in my actual requests. Generic descriptions match generic requests; specific nouns are free matcher surface.
State what it does not do. "Read-only - reports findings, does not edit the draft" prevents a whole class of wrong invocation. Negative space is as useful as positive space.
Put literal trigger phrases in when_to_use. Mine lists fact-check, verify claims, check sources, "is this accurate". Not paraphrases — the actual words I type.
The failure mode to avoid is the description that describes the domain instead of the job. "Helps with documentation quality" matches nothing in particular, which in a list of fifteen skills means it loses to all of them.
A quick before-and-after from my own first draft:
| Draft | Problem |
|---|---|
| "Fact-checks content for accuracy." | Describes a capability, names no artifact, no trigger phrase, could be any of five skills |
| "Fact-check a blog post or PRD draft before publishing. Verifies every number... Read-only - reports findings, does not edit the draft." | Names the moment, the artifacts, the output, and the boundary |
Skill, Subagent, Command, or Hook?
Four primitives, and picking wrong is the second most common way a skill fails.
Skill — a procedure that runs in your main conversation. Claude keeps full access to the conversation history, and all the output lands in your thread where you can see it. This is right when the result is something you need to read and act on.
Subagent — an isolated context window that returns a summary. The documented triggers:
Use subagents when: The task produces verbose output you don't need in your main context. You want to enforce specific tool restrictions or permissions. The work is self-contained and can return a summary.
And the explicit pointer back:
Consider Skills instead when you want reusable prompts or workflows that run in the main conversation context rather than isolated subagent context.
The two are not exclusive — a skill can set context: fork to run its own body inside a subagent, which is the right move when the skill is the thing producing verbose output.
Hook — fires automatically on lifecycle events. If a rule must hold every single time regardless of what Claude decides, it is a hook, not a skill. "Block edits to protected files" is a hook. A skill is always a judgment call away from not running.
CLAUDE.md — facts, always loaded. Covered above.
For blog-factcheck: I want the verdict table in my main thread so I can act on it row by row, the output is a compact table rather than a log dump, and I do not want it firing automatically on every file write. Skill, main context, no fork.
Four Primitives, One Decision
Picking the wrong one is the second most common way a skill fails. The deciding question is when the thing needs to be in context — and whether it may be skipped.
CLAUDE.md
It is a fact, not a sequence of steps
Skill
You keep pasting the same instructions into chat
Subagent
The output is a log dump; you only want the summary
Hook
A judgment call away from not running is unacceptable
These are not exclusive. A skill can set context: fork to run its own body inside a subagent — the right move when the skill itself is what produces the verbose output.
What I Got Wrong
Three things, in the order they embarrassed me.
I wrote the description last. First draft was "Fact-checks blog content." I wrote 300 lines of procedure and then dashed off the one field that determines whether any of it executes. Write the description first now — it forces you to name the triggering moment before you have talked yourself into a scope.
I put the taxonomy in the body. My first SKILL.md was 180 lines, with the full source-tier breakdown inline. It worked, and it cost 180 lines of recurring context every time it loaded, most of it irrelevant to any given claim. Splitting the taxonomy into references/ cut the body to 70 lines with no loss of capability. The test I use now: if Claude doesn't need this to execute the happy path, it belongs in a reference file.
My script had a substring bug that survived three test runs. I matched qualifier words with a naive term in text check. It dutifully flagged the word "may" inside "Alpamayo" and "all" inside "actually" on every pass. Fixed with a word-boundary regex:
def find_terms(text, terms):
"""Match whole words only - substring matching flags 'may' inside 'Alpamayo'."""
low = text.lower()
found = set()
for term in terms:
if re.search(rf"(?<![\w-]){re.escape(term)}(?![\w-])", low):
found.add(term)
return sorted(found)
And then it caught me. Before publishing, I ran blog-factcheck on this post. It flagged 8 quantitative claims, 6 of which traced cleanly to the docs or to measurements of the skill itself. The other two were mine: a "60% justification" statistic I had invented to sound authoritative, and a reference to feeding Claude a "9,000-word document" when the draft in question is 1,310 words. Both are now fixed — the first softened to the opinion it always was, the second corrected. A post about factual grounding shipping with two fabricated numbers would have been a fitting way to lose the argument.
The deeper point is why the substring bug was findable. The bug lived in a Python file with deterministic output, so I could run it, read sixteen lines of output, and see the wrong answer immediately. Had the same logic lived as a paragraph of instructions in SKILL.md — "look for hedging language like 'may' or 'approximately'" — it would have failed quietly and differently every run, and I would probably still not know. Deterministic components fail loudly. That is a feature.
The Checklist
What I'd hand someone starting their first skill:
- Write the
descriptionfirst. Name the triggering moment, not the capability. If you cannot describe when it should fire, you are not ready to write the body. - Put literal trigger phrases in
when_to_use. The words you actually type. You have 1,536 characters across both fields; most skills should use a third of that. - Ask whether this is a fact or a procedure. Facts belong in
CLAUDE.md. Procedures belong in a skill. ACLAUDE.mdsection that has grown steps is a skill waiting to be extracted. - Keep the body under 500 lines, and aim far lower. State what to do. Delete every sentence explaining why it matters.
- Move the taxonomy, the rubric, and the examples to
references/. Link to them from one line in the body. - Push anything deterministic into a script. If it has a right answer, it does not need a language model. The script's source never enters context.
- Restrict
allowed-toolsto what the skill actually needs. Leaving outWriteis a product decision, not a limitation. - Set
disable-model-invocation: trueon anything destructive. Deploys, deletes, pushes — human-triggered only. - Run it on real input before you trust it. Mine had a substring bug that three synthetic tests missed and one real document caught in seconds.
The skill in this post is 373 lines, of which 70 load when it runs and 480 characters load when it doesn't. That ratio — a tiny always-on footprint buying access to a large on-demand capability — is the entire design goal. Get it right and you can install dozens of skills without noticing the cost. Get it wrong and you have either a skill that never fires or a CLAUDE.md with extra steps.
Sources: Claude Code documentation on Skills, the context window, subagents, and memory. Line counts and character counts are measured from the shipped skill in this repository.