installs.me
< cd ~/blog

·5 min read#skills#how-to

How to Write a Claude Code Skill: SKILL.md From Scratch

A Claude Code skill is one Markdown file, and the part most people get wrong is not the instructions. It is the 200 characters of YAML that decide whether the skill ever fires.

Here is the mechanism. Claude Code keeps the name and description of every installed skill in context at all times. The body of SKILL.md is loaded only when Claude decides the skill matches the current request. That means the frontmatter is your trigger surface and the body is your payload, and they are read at completely different times. Write them for different audiences: the description for a model scanning fifty skill summaries, the body for a model that has already committed to the task.

Where skills live

Three places, same file format:

  • .claude/skills/<name>/SKILL.md in a repo (project skills, checked in, shared with the team)
  • ~/.claude/skills/<name>/SKILL.md (personal, follows you everywhere)
  • skills/<name>/SKILL.md inside a plugin (distributable, installed via a marketplace)

We will build a project skill, since that is the shortest loop, then note what changes when you ship it as a plugin.

The anatomy

The example: a skill that writes release notes in your team's house style. Full directory tree:

.claude/skills/release-notes/
├── SKILL.md
└── references/
    ├── style-guide.md
    └── good-examples.md

And the real SKILL.md:

---
name: release-notes
description: >
  Draft release notes in Acme's house style from merged PRs or a git
  range. Use when the user says "write the release notes", "draft the
  changelog", "what shipped this week/sprint", or pastes a list of
  merged PRs and asks to summarize them for users. Do NOT use for
  internal standups or commit messages.
---

# Release notes

Write user-facing release notes from a set of changes.

## Process

1. Get the changes. If the user gave a git range, run
   `git log --oneline <range>`. If they gave PR numbers, run
   `gh pr view <n> --json title,body` for each.
2. Bucket every change into exactly one of: Features, Fixes,
   Breaking. Drop pure-internal changes (CI, refactors, deps)
   unless they change user-visible behavior.
3. Read references/style-guide.md and follow it exactly.
4. Write one bullet per change, verb-first, user-benefit framing.
   If a bullet needs the word "refactor", it does not belong here.
5. If the output feels off, compare against
   references/good-examples.md before rewriting.

## Output

A single Markdown block the user can paste into the GitHub release.
No preamble, no "here are your release notes".

That skill works today. Drop it in, open a fresh session, type "what shipped this week", and it fires.

The description is a routing function

Everything in the description field exists to answer one question: given this user message, is this skill relevant? So write it like a classifier spec, not marketing copy.

Three things that measurably improve triggering:

Literal trigger phrases. "Use when the user says 'draft the changelog'" beats "helps with changelogs". The model is matching the user's actual words against your description. Quote the words.

Input shapes, not just intents. "Or pastes a list of merged PRs" catches the case where the user gives you data with no verb at all. Users do this constantly.

Negative triggers. "Do NOT use for commit messages" prevents the adjacent-but-wrong invocation, which is the most common failure mode once you have more than a handful of skills. Two skills with overlapping descriptions will steal each other's traffic.

Keep name boring and kebab-case. It is an identifier, not a headline.

The body is instructions, not documentation

Once the body loads, you are talking to a model that has agreed to do the job. Skip the throat-clearing and give it a procedure. The patterns that hold up:

Numbered steps with real commands. git log --oneline <range> is executable; "gather the relevant changes" is a vibe. Concrete commands also make the skill debuggable, because you can see which step went wrong.

Hard rules stated as hard rules. "If a bullet needs the word 'refactor', it does not belong here" survives contact with the model. "Try to keep it user-focused" does not.

An explicit output contract. "No preamble" saves you from three sentences of filler on every invocation.

references/ is your context budget

The references/ directory is progressive disclosure, level three. Frontmatter is always in context. The body loads on trigger. Reference files load only when the body tells Claude to read them, via a plain instruction like "Read references/style-guide.md".

Use it for anything long: a 2,000-word style guide, ten example outputs, an API schema. Putting that content in the body means paying for it on every invocation, including runs that never needed it. Putting it in references means the model pulls it only on the steps that require it. A good heuristic: SKILL.md under 100 lines, everything else in references, linked from the exact step where it matters.

Shipping it as a plugin

The same directory becomes distributable with almost no changes. A plugin is a repo containing a .claude-plugin/plugin.json manifest, your skills under skills/, and optionally commands, hooks, and MCP servers (those are the four plugin surfaces). Point a marketplace.json at it and anyone installs with:

/plugin marketplace add <your-marketplace-url>
/plugin install release-notes@<marketplace>

One sharp edge, verified on Claude Code CLI 2.1.x: plugin sources referenced over a URL must be git-backed (the git-subdir source type). A relative HTTP path in marketplace.json fails silently, and you will spend an hour wondering why the install did nothing.

Test the trigger, not the output

The last-mile check most people skip: open a fresh session and say the trigger phrase without naming the skill. If Claude does not reach for it, your description lost the routing decision, and no amount of body polish will save it. Iterate on the description the way you would iterate on a prompt, because that is exactly what it is.

Install a person

installs.me turns your files, calendar and calls into a Claude Code plugin that thinks like you. Anyone installs it with two commands:

/plugin marketplace add https://installs.me/lautaro
/plugin install lautaro@lautaro-installs