Claude Code skills: SKILL.md, triggers, and context cost
A skill is a SKILL.md file with instructions that Claude loads when the task matches or when you type its name. Its body costs nothing until it runs. Its description costs you every turn. That second fact is the one most skill guides skip, and it's where the real decisions are.
I ran /skill-doctor on this machine before writing this. It found 66 skills across my user directory and the two plugins that ship skills, 60 of them in the listing that Claude sees every turn, adding up to roughly 4,460 tokens of descriptions per request. Nine had never been invoked. One skill, brainstorming, had fired 148 times. The xlsx skill costs 320 tokens of listing on every turn and I use it about once a week. That report is the whole argument of this post.
A skill earns its place by triggering when it should and staying out of the way when it shouldn't. Write the body for the day it runs. Write the description for the 10,000 turns it doesn't.
What a skill is, and what happened to commands
A skill is a directory containing SKILL.md, with YAML frontmatter that tells Claude when to use it and a markdown body that tells Claude what to do. Custom commands were merged into skills, and both still work.
The official skills reference, read on 2026-09-07, states it plainly: a file at .claude/commands/deploy.md and a skill at .claude/skills/deploy/SKILL.md both create /deploy and behave the same way. Command files support the same frontmatter, except name and paths. What skills add is a directory for supporting files, frontmatter that controls who invokes them, and automatic loading when Claude judges the skill relevant. I covered the migration itself in the Claude Code commands post. This one is about what to do with the extra room.
Claude Code skills follow the Agent Skills open standard, which works across tools, and OpenAI's implementation is close enough that most of a Codex skill ports with a frontmatter edit. Three distribution paths, claude.ai uploads, the Skills API, and packaging with package_skill.py, accept only the standard's six fields: name, description, license, compatibility, metadata, and allowed-tools. Claude Code accepts more, and any extra field fails those paths with an Unexpected key(s) error. If a skill might travel by one of them, stay inside the six.
Where a skill lives decides who gets it. Enterprise skills come from managed settings. Personal skills sit in ~/.claude/skills/<name>/SKILL.md. Project skills sit in .claude/skills/ and ship with the repository. Plugin skills are namespaced as /plugin:skill so they never collide. On a name clash, enterprise beats personal beats project, a skill beats a command, and any of them beats a bundled skill of the same name but not its alias, so a project code-review replaces /code-review while /review still runs the bundled one. Nested .claude/skills/ directories in a monorepo load the first time Claude reads a file in that subdirectory and appear under a qualified name like apps/web:deploy.
Edits to SKILL.md are picked up live. Changes to a skill folder that's also a plugin, its hooks or .mcp.json, need /reload-plugins.
The frontmatter that changes behavior
Only description is recommended. Everything else is a control: who can invoke the skill, which tools it pre-approves, which model and effort it runs at, whether it runs in a subagent, and which files activate it.
---
name: release-check
description: Verify a web release before deploy. Use for "ready to ship", preflight, or release checks. Never deploys, commits, or pushes.
disable-model-invocation: true
allowed-tools: Bash(npm test *) Bash(git status *)
context: fork
agent: Explore
---The fields I set on purpose, with what the docs say about each:
disable-model-invocation: true means only you can run it. The description isn't even loaded into context, and Claude is told to ask you to run the skill rather than replicate its steps another way. Use it for anything with side effects: deploy, commit, send. The inverse, user-invocable: false, hides a skill from the / menu for background knowledge that isn't an action.
allowed-tools grants the listed tools for the turn that invokes the skill and clears on your next message. It doesn't restrict anything; every other tool stays subject to your permission settings. Two details matter. The grant isn't gated by workspace trust, so a project skill checked into a repository can pre-approve tools in a folder you never trusted, and you should read allowed-tools before running Claude Code in someone else's repo. And the ${CLAUDE_SKILL_DIR} substitution works inside the rule, so Bash(${CLAUDE_SKILL_DIR}/scripts/run.sh *) lets a bundled script run without a prompt wherever the skill is installed.
model and effort override for the rest of the turn and aren't saved. A model excluded by your organization's allowlist is ignored and the session keeps its own. Frontmatter model on interactive sessions was broken until v2.1.259, which is the kind of thing you find out by reading the changelog rather than the reference.
context: fork runs the skill in a subagent with the skill body as its prompt. Since v2.1.218 that fork runs in the background by default, and background: false makes the turn wait. In -p mode and the Agent SDK it always waits. A backgrounded fork gets the narrower tool set that background subagents get, so a skill whose steps need a tool outside that set must set background: false. The docs' warning is right: a fork only makes sense for a skill with an explicit task, because a guidelines-only skill gives the subagent nothing to do.
paths limits automatic activation to work on matching files, using the same glob format as path-scoped rules. hooks registers hooks when the skill is invoked and keeps them for the session. shell: powershell switches the injected commands to PowerShell where that tool is enabled.
Dynamic context and what happens when it fails
A line starting with !`command` runs before the skill content reaches Claude and is replaced by the output. A failed command aborts the whole invocation. Claude never sees the skill.
## Current changes
!`git diff HEAD`
## Instructions
Summarize the changes above and list anything risky.This is the feature that makes a skill grounded instead of generic: the diff, the test output, the current branch, inlined before the model reads a word. The rules are strict. The ! must start a line or follow whitespace. Substitution runs once, so a command can't emit a placeholder for a later pass. Each command runs in the session shell's working directory, under the Bash tool's two-minute timeout, with stderr merged into stdout. Any non-zero exit fails the invocation with Shell command failed for pattern, except exit 1 from search commands like grep, so append || true to a check script that exits 1 on findings. And the permission check must return allow: a command that would ask you aborts instead, so pre-approve it in allowed-tools.
Skills synced from your claude.ai account are treated differently on your machine. Claude Code doesn't run their ! commands, doesn't attach their @ references, and escapes their descriptions, because you didn't write those files here. disableSkillShellExecution turns injection off for user, project, plugin, and additional-directory skills, bundled and managed skills excepted, which is what a managed policy uses.
The lifecycle nobody reads
Invoked skill content enters the conversation once and stays there across turns. Claude Code doesn't re-read the file on later turns. After compaction, each skill keeps its first 5,000 tokens, within a shared 25,000-token budget, most recent first.
Two consequences. Write standing instructions, not one-time steps, because the content persists but the allowed-tools grant doesn't. And keep the body short, because every line is a recurring cost for the rest of the session. Re-invoking a skill whose rendered content is unchanged adds a one-line note rather than a second copy. If a skill seems to stop working after the first turn, the content is almost always still there and the model is choosing other tools; the fix is a sharper description, or a hook if the behavior must be enforced.
Descriptions are routing code, and they get truncated
Claude Code loads every skill's name and description into context so Claude knows what exists. The listing has a budget of 1% of the model's context window. When it overflows, descriptions are dropped starting with the skills you invoke least, and every name always stays.
The combined description and when_to_use text is capped at 1,536 characters per skill regardless of budget, so put the key use case first. /context shows the Skills row after the budget is applied, matching what the model receives. /doctor estimates the listing cost and its biggest contributors. And /skill-doctor, listed in the changelog under v2.1.261 on 2026-09-04 and documented as requiring v2.1.252 or later, shows what each skill costs and how often it has been used, so you can decide what to turn off. In an interactive session the report opens in the /plugin manager's Stats tab; with -p it prints as text, which is how I got the numbers at the top of this post.
Here's what that report looked like for me, condensed:
| skill | listing cost per turn | invocations | last used |
|---|---|---|---|
| brainstorming | ~70 tokens | 148 | 62 days ago |
| codex:rescue | ~40 | 132 | 63 days ago |
| writing-plans | ~30 | 92 | 20 days ago |
| xlsx | ~320 | 52 | 24 days ago |
| doc-coauthoring | ~150 | 0 | never |
| theme-factory | ~90 | 0 | never |
The nine never-invoked skills cost about 800 tokens a turn between them. Over a long session that's real money and, more to the point, it's noise in the one list Claude uses to decide what to load. skillOverrides in settings fixes it without editing anyone's SKILL.md: "name-only" keeps the name and drops the description, "user-invocable-only" hides it from Claude, "off" hides it everywhere. The /skills menu writes those values for you; highlight a skill, press Space to cycle its state, Escape to save.
The inverse problem is a skill that never fires. When the frontmatter YAML is malformed, Claude Code loads the body with empty metadata, so /name works but there's no description to match against and the skill never triggers on its own. claude plugin validate .claude/skills finds those files, since v2.1.233. Test both directions in a fresh session: a prompt that should trigger the skill and one that shouldn't, with the skill enabled and then disabled. The skill-creator plugin automates that loop, records pass rates and token cost with and without the skill, and runs a blind A/B between two versions before you commit an edit.
What we run: skills as orchestrators
On the production platform we operate for clients, a skill is the orchestrator of a content pipeline. It reads the brief, spawns the researcher and writer subagents in order, runs the audit, invokes the editor gate, and hands off to publishing. The subagents do the work. The skill holds the procedure.
Two such skills exist on one server, one for guest posts and one for owned-brand articles, sharing worker agents but differing in the gate. The owned-brand one opens like this, brand name removed:
---
name: inhouse-blog-writer
description: Write an owned-brand blog post from a content sheet row. Keyword-driven, references-grounded, format-specific. Runs research, draft, anti-AI audit, senior editor.
not_for: Guest posts for client outreach (use guest-post-writer). Anchor-driven link-building articles.
---The not_for field is our own convention, not Claude Code frontmatter, but it does the same job as a sharp description: it tells the model when to leave the skill alone. The owned-brand skill sends a draft back to the writer at most twice before it flags the piece for a human. That number lives in the skill, not in the writer, because the writer shouldn't know how many chances it gets.
The pattern matches the docs' own advice on where things belong. A fact goes in CLAUDE.md. A procedure that has grown past a few lines goes in a skill, where its body loads only when it runs. The memory system is for what Claude should hold every session; skills are for what it should do when asked. On this machine that boundary is why my user CLAUDE.md stays at 6.4 KB while the reference material behind it runs to tens of pages: the heavy documents are skills and imports that load on demand.
One more thing skills are for, which took me too long to see. A skill that spawns a subagent with context: fork and agent: Explore is the cheapest way to run a read-only investigation, because Explore runs read-only and, in my setup, carries no Agent tool. The four fact-checks that ran against our existing posts while I wrote this were exactly that shape, and none of them wandered.
We publish the skills we run in production as an installable library at Scalably Skills Library.
Frequently asked questions
What is a Claude Code skill?
A skill is a directory containing a SKILL.md file whose YAML frontmatter tells Claude when to use it and whose markdown body tells Claude what to do. Claude loads the body when the task matches the description or when you type /skill-name. Skills replaced custom commands, and existing command files keep working.
Where do Claude Code skills live?
Personal skills live in ~/.claude/skills/<name>/SKILL.md and apply to all your projects. Project skills live in .claude/skills/ and ship with the repository. Plugin skills are namespaced as /plugin-name:skill-name. Enterprise skills are deployed through managed settings and take precedence over personal and project skills with the same name.
How much context do skills use?
Each skill's name and description load into context every turn, within a listing budget of 1% of the context window, and the full body loads only when the skill is invoked. Run /skill-doctor to see what each skill costs and how often it's used, or /context to see the size of the listing the model receives.
How do I stop Claude from running a skill automatically?
Set disable-model-invocation: true in the skill's frontmatter so only you can run it with /skill-name. To do it without editing the file, set the skill to "user-invocable-only" under skillOverrides in your settings, or press Space on it in the /skills menu.
What is the difference between a skill and a subagent?
A skill is instructions loaded into the current conversation. A subagent is a separate agent with its own context window that returns a summary. A skill with context: fork runs its instructions inside a subagent, and a subagent definition with a skills field preloads skill content at startup, so the two compose in both directions.