How to write an agent skill (SKILL.md) that a model actually follows
A skill is a folder with one Markdown file in it, and most of them are written like a wiki page. Here is the shape that gets loaded at the right moment, followed when it loads, and ignored the rest of the time — which is the whole point.
A skill does not give a model a new ability. It gives it a procedure — a way of doing something it could already do badly — and it does so by putting text into the context window at the moment a task matches. That last clause is the whole design. A skill that is loaded on every session is a longer system prompt with a folder around it, and a skill that never loads is a document nobody reads. Writing one well is mostly writing for the moment of loading.
The shape
The format has converged across the agents that read one: a directory named for the skill, a SKILL.md at its root with YAML front-matter, and any scripts, templates or reference documents beside it. In Claude Code the folder lives under .claude/skills/ in a project or under ~/.claude/skills/ for every project; other agents read equivalent instruction files from their own places, and the writing advice is the same for all of them.
release-notes/
├── SKILL.md
├── template.md
└── scripts/
└── changelog-since.sh
# SKILL.md
---
name: release-notes
description: Writes release notes from merged pull requests since the last tag. Use when asked for release notes, a changelog entry, or "what shipped".
---
## When this applies
The user wants a human-readable account of what changed between two versions.
## Steps
1. Run `scripts/changelog-since.sh <tag>` to list merged PRs with their labels.
2. Group by label: Added, Changed, Fixed, Security. Drop chore and ci.
3. One line per PR, user-facing wording, no PR numbers in the sentence — the number goes in parentheses at the end.
4. Fill `template.md`. Do not invent a version number; ask if there is no tag.
## Do not
- Do not describe internal refactors unless they changed behaviour.
- Do not run the script against a dirty working tree.Progressive disclosure is why the description matters most
The agent does not read every skill on every turn. It reads the front-matter of all of them once — the name and the description — and that is all it knows about a skill until a task matches. Then it loads the body. Then, if the body says so, it reads the template or runs the script. Each layer costs context only when it is reached.
So the description is not a summary. It is the routing rule, and it is competing with every other description in the folder for the model’s attention on every turn. The one that works names the task the user has, in the words the user would use, and stops. "Writes release notes from merged pull requests. Use when asked for release notes, a changelog entry, or what shipped" routes. "A comprehensive tool for managing your project’s release communication workflow" does not, because no user has ever typed that.
- Name the trigger, not the topic. "Use when the user asks to…" is the sentence that decides. Put it in.
- Include the words people actually use, including the sloppy ones: "changelog", "what shipped", "release notes". The model matches on them.
- Say what it is not for when a neighbour exists. Two skills that both mention "tests" will both fire, and the wrong one first.
- Keep it under three sentences. It is read on every session by every agent that has the folder, whether or not the skill is ever used.
The body is a procedure, not a document
Once loaded, the body is read as instruction by a system that follows instructions literally and cannot ask what you meant. That rules out most of what people put in a wiki: background, rationale, history, options. What survives is the shape of a good runbook — numbered steps, the exact command, the file to fill in, and the conditions under which to stop and ask.
- Steps in order, one action each. A model given a paragraph picks an order. A model given a list follows it.
- The exact command, with the exact flags. "Run the changelog script" is an invitation to guess; scripts/changelog-since.sh v1.4.0 is not.
- Refusals, stated as refusals. "Do not invent a version number; ask" is worth more than a paragraph about why version numbers matter.
- Stop conditions. When the working tree is dirty, when the tag is missing, when there are more than fifty PRs — say what to do, because the model will otherwise do something.
- An example of the output. One finished release-notes block in the body or in a template file beats any description of what one should look like.
Scripts do the parts models do badly
A model reading forty pull requests and grouping them by label will get it nearly right, differently each time. A twelve-line shell script gets it exactly right every time and costs one tool call rather than forty pages of context. The pattern is to push everything deterministic — listing, filtering, formatting, measuring — into a script the skill calls, and keep the model for the part that is judgement: wording, grouping edge cases, deciding what a user would care about.
Two rules for those scripts. They run with whatever permissions the agent has, so they should do one thing and fail loudly rather than improvise. And they are the part of a skill that a reviewer has to read as code rather than prose, because a skill folder from somebody else is code from somebody else, whatever the file extension says.
Testing a skill without guessing
A skill has two failure modes and they need two tests. Does it load when it should — and only then? Does it produce the right thing when it does? Both are cheap to check by hand and almost nobody does.
| Test | How | What a failure looks like |
|---|---|---|
| Fires on the right ask | Five phrasings a real user would type, from "release notes" to "what changed since Tuesday" | Loads for three of five — the description is missing their words |
| Stays quiet on the wrong ask | Five neighbouring tasks: a commit message, a PR description, a changelog for a library you do not own | Loads for a commit message — the description says "changes" and nothing narrows it |
| Does the steps in order | Watch the transcript once; look for a step skipped or reordered | Wrote the notes before running the script, from memory of the PRs |
| Stops where told | Run it with no tag, and with a dirty tree | Invented v1.5.0; committed the template |
The five patterns that make a skill worse than nothing
- The everything skill. One folder that covers "engineering practices" fires on every task and steers all of them slightly. Split it into the six procedures it actually contains.
- The description that is a mission statement. If it could be the first line of a README, it is not a routing rule.
- Background as instruction. Three paragraphs on why the team adopted conventional commits, read on every commit, changing nothing about the commit.
- Addressing the model. "You are an expert release manager" is a costume; "run the script, then fill the template" is a procedure. Only one of them changes the output.
- Instructions to ignore other instructions. A skill that tells the model to override the user, the system prompt or another skill is not a skill; it is the thing every reviewer of a skill folder is looking for.
The test of a good skill is dull: it loads for the task it names, it does the steps in the order written, it stops where told, and it is not there the rest of the time. Write for the moment of loading, keep the body to what a runbook would say, put the deterministic parts in a script, and read the folder as code before you install it — yours included.