SKILL.md Guide: Write a Good Skill for Coding Agents

A SKILL.md file holds on-demand instructions for a coding agent. This guide covers the format, the description that triggers it, a validator and two tests.

Diagram of a skill folder named release-notes with a required SKILL.md file holding YAML frontmatter and a Markdown body, and optional scripts, references and assets folders.

An agent skill refers to a folder of instructions for one repeating task, which the agent loads only when that task comes up. Its entry file is SKILL.md. The file has two parts, a YAML frontmatter block with a name and a description, and a Markdown body with the instructions. The format is the open Agent Skills specification.

So the description is the most important part of a skill, because agents use the description to decide whether to load the skill and read any of its instructions. A skill with perfect instructions and a vague description never runs.

We write a skill when a task repeats and takes several steps, such as writing release notes or reviewing a pull request. The following example is the smallest useful one. Claude Code reads .claude/skills and Codex reads .agents/skills, so the folder depends on the agent, and section 1.1 lists the rest.

---
name: release-notes
description: Writes release notes from the git commits since the last tag. Use when the user asks for release notes or a changelog entry.
---

1. Run `git log --oneline` from the last tag to HEAD.
2. Group the commits under Added, Fixed and Changed.
3. Write one line per commit in the past tense.

Three things make this example work.

  • The description says what the skill does and when to use it, and that one line is what the agent matches a task against before it loads anything else.
  • The body starts with numbered steps instead of background, so the agent runs the steps in order rather than working out what our paragraphs mean.
  • The steps name one way to do the job, so the agent uses the method we allow instead of looking for another solution of its own.

The sections below cover the file format, then how to write each of these three parts, then a release-notes skill we build and test.

1. Why a Skill Instead of a Longer Prompt

Without skills we have two options. Either we retype the instructions in every prompt, or we put them in a file that the agent reads at the start of every session. Problem with the second one is that the agent reads that file even in sessions that never need it.

A skill is the third option. It holds the instructions for one task along with the files that task needs, and the agent opens it only when the task comes up. Twenty installed skills cost about as much context as one paragraph, because the agent holds only their names and descriptions until one of them matches.

Let’s say a team ships a library every two weeks and the release notes always follow the same format. Without a skill, somebody pastes that format into a prompt every time. With a skill, the team commits the format once and every developer’s agent follows it.

1.1. Which Tools Read a SKILL.md

Support is wide. The folder names are not the same in every tool, and that is the one thing to check before you write anything. The table lists the folders and the extra frontmatter fields of six coding agents, taken from each tool’s own documentation on 17 October 2026.

ToolProject foldersPersonal foldersAdditions to the open format
Claude Code.claude/skills, nested .claude/skills in subfolders, plugin folders~/.claude/skillsFields such as when_to_use, disable-model-invocation, user-invocable, paths, context, hooks and model. A custom command file and a skill with the same name both create one slash command, and the skill wins.
Codex.agents/skills in every folder from the working directory up to the repository root$HOME/.agents/skillsOptional agents/openai.yaml with policy.allow_implicit_invocation and tool dependencies. The skill list is limited to 2% of the context window.
GitHub Copilot and VS Code.github/skills, .claude/skills, .agents/skills~/.copilot/skills, ~/.agents/skills, and in VS Code also ~/.claude/skillsFields argument-hint, user-invocable, disable-model-invocation and context. Skills work in the Copilot cloud agent, code review, the CLI, and agent mode in VS Code and JetBrains IDEs.
Cursor.agents/skills, .cursor/skills, and for compatibility .claude/skills and .codex/skills~/.agents/skills, ~/.cursor/skillsFields paths, disable-model-invocation, icon and color. The built-in command /migrate-to-skills converts eligible rules to skills.
Gemini CLI.gemini/skills, .agents/skills~/.gemini/skills, ~/.agents/skillsThe model calls the tool activate_skill, and the user confirms the skill name and folder before the body loads.
OpenCode.opencode/skills, .claude/skills, .agents/skills~/.config/opencode/skills, ~/.claude/skills, ~/.agents/skillsKeys such as opencode/slash inside metadata, and allow, ask or deny rules for skills in opencode.json.

Several tools read .agents/skills, and several also read .claude/skills, so one folder can serve more than one agent. The Agent Skills site lists other clients, including JetBrains Junie, Amp, Goose, Roo Code, Kiro, Factory, Spring AI and Mistral Vibe. Note that a tool ignores or rejects a field it doesn’t know, so a skill meant for more than one tool uses the open fields only.

2. The SKILL.md File Format

The folders differ between tools, but the file itself does not. The format fixes three things and leaves everything else open.

  • The file is named SKILL.md.
  • It starts with a YAML frontmatter block.
  • The Markdown body after the frontmatter has no restrictions at all.

Everything else in the folder is optional and follows conventions the specification recommends.

2.1. The Skill Folder

A skill folder holds SKILL.md and up to three more folders. The folder name has to match the name field in the frontmatter. A mismatch is the most common reason a skill loads nowhere and doesn’t give you an error.

Diagram of a skill folder named release-notes with a required SKILL.md file holding YAML frontmatter and a Markdown body, and optional scripts, references and assets folders.
A skill is a folder, and only SKILL.md is required.
release-notes/
  SKILL.md          required, frontmatter and instructions
  scripts/          optional, code the agent can run
  references/       optional, documents the agent reads when needed
  assets/           optional, templates, images and data files

Two rules govern the paths inside SKILL.md.

  • Every path is relative to the skill folder, such as scripts/group_commits.py.
  • References stay one level deep. So SKILL.md links to a file, and that file does not link on to further files.

2.2. Frontmatter Fields

The frontmatter sits between two lines that contain nothing but three hyphens. The format defines six fields, and only name and description are required.

FieldRequiredRulePurpose
nameYes1 to 64 characters. Lowercase letters, digits and single hyphens. No hyphen at the start or the end. Same as the folder name.Identifies the skill and becomes the command name in many tools.
descriptionYes1 to 1,024 characters, not empty.Says what the skill does and when to use it. The agent matches the task against this text.
licenseNoA license name or the name of a bundled license file.States the terms for sharing the skill.
compatibilityNo1 to 500 characters.States environment needs such as git, Python 3.10 or network access.
metadataNoA map from string keys to string values.Holds extra properties such as author and version. Keys should be unique to avoid conflicts.
allowed-toolsNoA space-separated string. Experimental.Lists tools that are approved without a prompt, such as Read. Support differs between agents.

Tools add fields of their own, as the table in section 1.1 shows. Claude Code ignores a field it doesn’t know. Uploading a skill to claude.ai, and the Skills API that installs one over HTTP, both reject it instead. So a skill you plan to share uses the six open fields only.

2.3. Rules for the name Field

The specification puts five rules on the name field. A name that breaks any of them gets the skill rejected, and the error message doesn’t always say which rule broke.

  • The name is 1 to 64 characters long.
  • It holds lowercase letters, digits and hyphens only. No uppercase, and no underscore.
  • It does not start or end with a hyphen.
  • It has no two hyphens in a row.
  • It matches the folder name exactly.
name: pdf-processing     # valid
name: code-review        # valid
name: PDF-Processing     # invalid, uppercase letters
name: -pdf               # invalid, starts with a hyphen
name: pdf--processing    # invalid, consecutive hyphens
name: release_notes      # invalid, underscore

2.4. The Markdown Body

Everything after the frontmatter is the body, and the agent loads all of it the moment the skill activates. The specification recommends three things in it, which are step-by-step instructions, examples of inputs and outputs, and the edge cases that catch people out. Section 5 covers how to write each of those.

3. How an Agent Loads a Skill

The agent loads the whole body at activation. So the question is what the agent holds before that, and the answer is progressive disclosure. The agent reads information in stages and opens more detail only when the task needs it, which is why most installed skills stay closed and cost almost nothing. The specification names three levels.

  • Level 1, the metadata. The name and description of every installed skill, loaded at startup. A typical description costs about 100 tokens.
  • Level 2, the body. The whole Markdown body of one skill, loaded when the task matches its description. The specification recommends keeping the body under 5,000 tokens and 500 lines.
  • Level 3, the files. Anything in scripts/, references/ and assets/, read only when the body points the agent at it.
Three boxes showing the loading levels of an agent skill, metadata at startup, the SKILL.md body when the task matches, and scripts, references and assets only when the body points to them.
The agent loads a skill in three levels, and each level is read only when the previous one shows it is needed.

Claude Code and Codex both cap the Level 1 text, so a long description can be cut before the agent ever sees the end of it. The table lists what each tool allows.

LimitValueSource
Description length1,024 charactersAgent Skills specification
Description plus when_to_use in the skill list1,536 characters per skill, and the whole list uses 1% of the context windowClaude Code
Skill list at startup2% of the context window, or 8,000 characters when the size is unknown. Descriptions are shortened first.Codex
Body after compactionThe first 5,000 tokens of each invoked skill, within a shared budget of 25,000 tokensClaude Code

The last row of the table is about compaction, where a long conversation is summarized to free up context. An invoked skill keeps only its first 5,000 tokens through that summary. Tools shorten descriptions first when the list grows, so we put the main use case at the start of the description.

4. Skill, AGENTS.md, CLAUDE.md and Custom Commands

All these four mechanisms give an agent instructions, and they differ in when the agent reads them.

  • An instruction file is read at the start of a session, so it suits the rules that apply to every task.
  • A skill is read when a task matches it, so it suits a procedure that only some tasks need.
  • A custom command is read when you type its name.

4.1. When Each One Loads

AGENTS.md is a README for agents. It sits at the root of a repository and tells any coding agent how to work in that repository, such as the build command, the test command and the code style. It is plain Markdown with no required fields. The agent reads the nearest AGENTS.md in the directory tree, and an instruction in the chat overrides the file.

CLAUDE.md does the same job in Claude Code, which also reads an AGENTS.md when the repository has one, through an @AGENTS.md import in the memory file.

PropertySKILL.mdAGENTS.mdCLAUDE.mdCustom commandPrompt
LoadedOn demand, when the task matches or the user invokes itAt the start of a sessionAt the start of every sessionWhen the user types the slash commandWhen the user sends it
ContentFrontmatter plus Markdown, with optional scripts, references and assetsMarkdown, no required fieldsMarkdown, imports with @pathOne Markdown prompt fileFree text
Best forA repeated multi-step procedureBuild commands, test commands, code styleThe same as AGENTS.md, for Claude CodeA prompt the user starts by handA one-time request
Size advice500 lines or fewer for the bodyNot set by the formatUnder 200 lines per fileShortShort
Read byTools listed in section 1.1Codex, Cursor, Copilot, Amp, Zed, Warp, opencode and others on agents.mdClaude CodeClaude Code, Copilot prompt files, Cursor commandsEvery agent

In Claude Code, the file .claude/commands/deploy.md and the skill .claude/skills/deploy/SKILL.md both produce the command /deploy, and the skill takes precedence. Cursor converts eligible rules into skills with its /migrate-to-skills command, and sets disable-model-invocation to true so the converted skills stay manual.

4.2. Choosing Between Them

In practice the choice depends on how often you need the instruction and how many steps it takes. So work through these questions in order, and stop at the first one that fits. The order matters, because an instruction that every task needs belongs in an instruction file even when it also takes five steps.

  1. Does the instruction apply to every task in the repository, such as the build command or the code style? Put it in AGENTS.md or CLAUDE.md.
  2. Will you need it again in a later session, and does it take five or more steps? Write a skill.
  3. Is it a one-off, or something you always want to start by hand? A prompt or a custom command is enough. A hook, which is a script the tool runs automatically on an event such as before a command executes, covers the cases where nothing should depend on the model deciding.
Decision tree that asks whether an instruction is needed in every task, needed again in later sessions, and needs files, scripts or five or more steps, ending in AGENTS.md, a prompt or a skill.
A skill fits an instruction that is needed in some tasks and comes with files, scripts or many steps.

5. Writing a Good SKILL.md

The format is settled, so the rest is a series of choices. The subsections below run in the order those choices matter, starting with the description, because the description decides whether the skill runs at all.

5.1. The description Field

The agent matches the task against the description and nothing else. So a description carries three pieces, and the format allows 1,024 characters for all of them. The good example below uses 315.

  • What the skill does.
  • When to use it.
  • When not to use it.
# Bad, says nothing about when to use it
description: Helps with releases.

# Good, says what, when and when not
description: Writes release notes for a git repository from its commit history since the last tag, grouped into Added, Fixed and Changed sections. Use when the user asks for release notes, a changelog entry, or a summary of what changed since the last release or tag. Do not use for commit messages or pull request descriptions.

Write the action in the third person, then put the trigger in a second sentence, the way the good example does. Note that the negative clause matters as much as the positive one. A skill that fires on every mention of the word release is worse than a skill that never fires.

5.2. Scope

One skill covers one task. A skill that needs two descriptions to explain itself is two skills. The trigger is the clearest test, so if the two halves of a skill fire on different requests, split the skill. If a sentence in the body applies to only half the cases, that sentence belongs somewhere else.

5.3. Steps, Gotchas and Examples

Numbered steps beat prose, because the agent follows steps in order and you can point at step 4 when something goes wrong. Some steps leave the agent room to choose the wording, and some have to run exactly as written.

Flexible, the agent chooses the wording:
  3. Write one line per commit in the past tense.

Fragile, the agent must not improvise:
  1. Run `python3 scripts/group_commits.py --repo . --format json` and read its output.
     Do not read the git log directly; the script decides which commits are skipped.

A gotcha is any place where the agent would otherwise guess wrong, and one line is usually enough. An example of the finished output teaches more than a paragraph describing it. So the body of a good skill ends with the shape of what the agent should produce.

5.4. Size Limits

The specification recommends a body under 500 lines and 5,000 tokens. Two habits keep a skill under those limits.

  • Cut the background the agent already knows.
  • Move the detail that only some runs need into references/, which section 5.6 covers.

5.5. Scripts for Deterministic Work

Some work has exactly one correct result, and a model that works it out again on every run will get it wrong eventually. Parsing commit messages is one of them. A regular expression either matches or it doesn’t. An agent reading raw git output can decide that chore: bump pytest belongs under Changed, and nothing in the skill catches that.

So we put that work in a script, and the step calls the script. A script in scripts/ can declare its own dependencies with PEP 723, which is a comment block at the top of the file. The package manager uv reads that block and builds a throwaway environment from it.

# /// script
# requires-python = ">=3.10"
# dependencies = ["pyyaml>=6,<7"]
# ///
import yaml

fields = yaml.safe_load("name: release-notes\nmetadata:\n  version: '1.0'")
version = fields["metadata"]["version"]   # '1.0'
print(version)
uv run read_version.py   # 1.0

The script runs with no installation step and no virtual environment of our own. If uv isn’t available, the alternative is pip install and a python3 call, and the skill says which one it expects.

5.6. References Loaded on Demand

A reference file is Level 3, so the file costs nothing until the body sends the agent to it. That only works if the body says when to go, which means a condition rather than a list.

Read references/commit-types.md only when the user asks why a commit is missing.
Read references/api-errors.md if the API returns a status other than 200.

5.7. A Bad and a Good SKILL.md

The two files below are the same skill written twice. Both come from the example project in section 6, where the bad version sits in bad-skill/release-helper. The good version uses Conventional Commits, a message format written as type(scope): text, for example fix(loans): return 404 for unknown member.

---
name: Release_Helper
description: Helps with releases.
---

Releases are an important part of software development. A release is a version of the software that is shipped to users. You should always try to write good release notes, because users like to know what changed. Be careful and make sure the notes are correct.

You can use git log, or git shortlog, or the GitHub API, or a changelog generator, or write the notes by hand. Pick whichever you prefer.

Write good notes.
---
name: release-notes
description: Writes release notes for a git repository from its commit history since the last tag, grouped into Added, Fixed and Changed sections. Use when the user asks for release notes, a changelog entry, or a summary of what changed since the last release or tag. Do not use for commit messages or pull request descriptions.
license: Apache-2.0
compatibility: Requires git and Python 3.10 or newer.
metadata:
  author: example-org
  version: "1.0"
---

# Release notes

## Steps

1. Run the helper script and pass the path of the repository. The script finds the last tag, reads the commits after it and groups them by Conventional Commit type.

   ```bash
   python3 scripts/group_commits.py --repo <repository path> --format json
   ```

2. Read the JSON. The key `skipped` lists commits that do not follow the Conventional Commit format. Mention them to the user and do not guess their type.
3. Fill in `assets/release-notes-template.md` with the grouped commits. Write one line per commit, in the past tense, without the commit type prefix.
4. If the JSON contains a commit with `"breaking": true`, put it first under a "Breaking changes" heading.
5. Show the result to the user and ask for the version number if the repository has no tag yet.

## Gotchas

- Merge commits are already excluded by the script. Do not add them back.
- The type `chore` and the type `docs` are left out of the notes on purpose. Read `references/commit-types.md` only when the user asks why a commit is missing.
- Never edit `CHANGELOG.md` unless the user asks for it.

## Example

Input commits since tag v1.2.0:

```text
feat(search): add author filter
fix(loans): return 404 for unknown member
chore: bump pytest
```

Output:

```markdown
## 1.3.0 - 2026-10-17

### Added
- Add author filter to search.

### Fixed
- Return 404 for unknown member in loans.
```
Problem in the bad versionFix in the good version
The name has uppercase letters and a low line character, and it differs from the folder name.The name is release-notes and matches the folder.
The description has 20 characters and gives no trigger.The description says what, when and when not.
The body explains what a release is, which the agent already knows.The body starts with steps.
Five tools are offered as equal options.One script is the default.
“Write good notes” cannot be checked.A template, an example and a list of gotchas define a good result.

6. A Release Notes Skill, End to End

The example project runs the good skill from end to end. It holds the skill, a helper script, a validator and a small git repository to run against. Everything ran on Python 3.13.16 and uv 0.13.1, and the scripts use the standard library only.

The demo repository comes from make_demo_repo.py, a script in the project that creates seven commits and tags the first one v1.2.0.

feat: add catalog search          <- tagged v1.2.0
feat(search): add author filter
fix(loans): return 404 for unknown member
perf(catalog): cache the genre list
chore: bump pytest
Update readme
refactor(api)!: rename borrow to checkout

6.1. The Helper Script

The script finds the last tag, reads the commits after the tag, and groups them by type. Two helpers do the git work, and both fail loudly instead of returning an empty result.

def git(repo, *args):
    result = subprocess.run(
        ["git", "-C", repo, *args], capture_output=True, text=True
    )

    if result.returncode != 0:
        print(f"Error: git {' '.join(args)} failed in {repo}: {result.stderr.strip()}",
              file=sys.stderr)
        sys.exit(2)

    return result.stdout.strip()


def last_tag(repo):
    result = subprocess.run(
        ["git", "-C", repo, "describe", "--tags", "--abbrev=0"],
        capture_output=True, text=True,
    )

    return result.stdout.strip() if result.returncode == 0 else None

The grouping itself is one regular expression and one dictionary. The full file is in the GitHub folder linked at the end.

SECTIONS = {"feat": "added", "fix": "fixed", "perf": "changed", "refactor": "changed"}
PATTERN = re.compile(r"^(?P<type>[a-z]+)(\((?P<scope>[^)]+)\))?(?P<bang>!)?: (?P<text>.+)$")


def collect(repo, since):
    rev_range = f"{since}..HEAD" if since else "HEAD"
    raw = git(repo, "log", rev_range, "--no-merges", "--pretty=format:%h%x1f%s%x1f%b%x1e")
    grouped = {"added": [], "fixed": [], "changed": [], "breaking": [], "skipped": []}
    for entry in filter(None, raw.split("\x1e")):
        short, subject, body = (entry.strip("\n").split("\x1f") + ["", ""])[:3]
        match = PATTERN.match(subject)
        if not match:
            grouped["skipped"].append({"hash": short, "subject": subject})
            continue
        breaking = bool(match["bang"]) or "BREAKING CHANGE" in body
        item = {"hash": short, "scope": match["scope"], "text": match["text"], "breaking": breaking}
        if breaking:
            grouped["breaking"].append(item)
        section = SECTIONS.get(match["type"])
        if section and not breaking:
            grouped[section].append(item)
    return grouped

Only feat, fix, perf and refactor map to a section. Every other type, including chore, docs, test and ci, is dropped on purpose. A commit that does not parse at all goes into a skipped list, and the agent reports that list.

6.2. Running the Steps

Step 1 of the skill calls the script with –format json, because JSON is what the agent reads. Note that the commit hashes differ on every run, because the demo repository is created fresh each time.

python3 make_demo_repo.py /tmp/bookshelf
python3 release-notes/scripts/group_commits.py --repo /tmp/bookshelf --format markdown
{
  "since": "v1.2.0",
  "added":    [ { "hash": "b3e8851", "scope": "search",  "text": "add author filter" } ],
  "fixed":    [ { "hash": "19b1101", "scope": "loans",   "text": "return 404 for unknown member" } ],
  "changed":  [ { "hash": "517d6a8", "scope": "catalog", "text": "cache the genre list" } ],
  "breaking": [ { "hash": "ddc0883", "scope": "api",     "text": "rename borrow to checkout" } ],
  "skipped":  [ { "hash": "e1d03ba", "subject": "Update readme" } ]
}

The same call with –format markdown prints the grouped commits as sections, which is useful while you test the script by hand.

Created /tmp/bookshelf
### Breaking changes
- rename borrow to checkout (c5fe964)

### Added
- add author filter (2b2eab6)

### Fixed
- return 404 for unknown member (2b072a4)

### Changed
- cache the genre list (e84ddc0)

Neither output is the finished release notes. Step 3 fills assets/release-notes-template.md, a file in the skill folder that supplies the version number, the date and the final wording.

## {version} - {date}

### Breaking changes
{breaking}

### Added
{added}

### Fixed
{fixed}

### Changed
{changed}

That split between the script and the agent is the part to copy. The script decides which commits count. The agent rewrites the text and fills the template. So the two parts fail in different ways, and you can tell which one broke.

Flow diagram of an agent running the release-notes skill, from the user prompt to matching the description, reading SKILL.md, running the script, reading the template and writing the notes.
The agent reads the template and runs the script only after SKILL.md tells it to.

The script reports every commit it couldn’t parse.

python3 release-notes/scripts/group_commits.py --repo /tmp/bookshelf | python3 -c "import json,sys; print(json.load(sys.stdin)['skipped'])"
[{'hash': '278d8a1', 'subject': 'Update readme'}]

A missing repository exits with code 2 and prints the git error.

python3 release-notes/scripts/group_commits.py --repo /tmp/nope   # exit code 2
Error: git log HEAD --no-merges --pretty=format:%h%x1f%s%x1f%b%x1e failed in /tmp/nope: fatal: cannot change to '/tmp/nope': No such file or directory

7. Validating a SKILL.md With Python

A broken skill gives you no error message to explain why, which is the reason to check it mechanically. The validator in the example project reads a skill folder and reports errors and warnings. It parses the frontmatter by hand instead of using a YAML library, which is why it needs nothing installed.

CheckLevelRule
SKILL.md exists and starts with frontmatterErrorThe first line is three hyphens
name formatErrorUp to 64 characters, lowercase letters, digits and single hyphens
name equals the folder nameErrorRequired by the specification
description lengthError1 to 1,024 characters
compatibility lengthErrorUp to 500 characters
Referenced files existErrorEach path under scripts/, references/ or assets/ in the body
description says when to use the skillWarningContains a phrase such as “Use when”
Body sizeWarning500 lines or fewer, about 5,000 tokens or fewer
Unknown frontmatter fieldWarningNot one of the six open fields
NAME_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
WHEN_HINT = re.compile(r"\b(use (this skill )?when|use for|use if|when the user)\b", re.IGNORECASE)


def check_name_and_description(fields, folder_name):
    errors, warnings = [], []
    name = fields.get("name", "")
    if not NAME_PATTERN.match(name) or len(name) > 64:
        errors.append(f"name '{name}' must use lowercase letters, digits and single hyphens, up to 64 characters")
    if name != folder_name:
        errors.append(f"name '{name}' does not match the folder name '{folder_name}'")
    description = fields.get("description", "")
    if not description or len(description) > 1024:
        errors.append(f"description has {len(description)} characters, it must have 1 to 1024")
    else:
        if len(description) < 40:
            warnings.append(f"description has only {len(description)} characters, add what the skill does and when to use it")
        if not WHEN_HINT.search(description):
            warnings.append("description does not say when to use the skill")
    return errors, warnings

Run the validator against the good skill and the two broken ones.

python3 validate_skill.py release-notes bad-skill/release-helper bad-skill/changelog--tool
release-notes
  SKILL.md: 1874 bytes, 43 body lines, about 348 tokens, description 315 chars
  OK
bad-skill/release-helper
  SKILL.md: 483 bytes, 6 body lines, about 104 tokens, description 20 chars
  ERROR   name 'Release_Helper' must use lowercase letters, digits and single hyphens, up to 64 characters
  ERROR   name 'Release_Helper' does not match the folder name 'release-helper'
  WARNING description has only 20 characters, add what the skill does and when to use it
  WARNING description does not say when to use the skill
bad-skill/changelog--tool
  SKILL.md: 218 bytes, 2 body lines, about 20 tokens, description 79 chars
  ERROR   name 'changelog--tool' must use lowercase letters, digits and single hyphens, up to 64 characters
  ERROR   referenced file 'scripts/make_changelog.py' does not exist
  ERROR   referenced file 'references/style.md' does not exist
  WARNING unknown frontmatter field 'colour' (agents may ignore it or reject it)

The second broken skill, bad-skill/changelog–tool, is the subtler failure.

---
name: changelog--tool
description: Creates changelog entries from commits. Use when the user asks for a changelog.
colour: blue
---

Run `scripts/make_changelog.py` and read [the style guide](references/style.md).

The description is fine. Problem with this one is the rest of the file, which breaks three things at once.

  • The name changelog–tool has two hyphens in a row.
  • colour is not one of the six defined fields.
  • Both referenced files, the script and the style guide, are missing.

So the agent loads the skill and then fails on the first step.

The specification also ships skills-ref, a reference implementation that catches the same name errors. It is a reference library rather than a released tool, so we treat it as a second opinion and keep our own validator for anything that gates a build.

uvx --from "git+https://github.com/agentskills/agentskills#subdirectory=skills-ref" skills-ref validate release-notes            # Valid skill: release-notes
uvx --from "git+https://github.com/agentskills/agentskills#subdirectory=skills-ref" skills-ref validate bad-skill/release-helper
Validation failed for bad-skill/release-helper:
  - Skill name 'Release_Helper' must be lowercase
  - Skill name 'Release_Helper' contains invalid characters. Only letters, digits, and hyphens are allowed.
  - Directory name 'release-helper' must match skill name 'Release_Helper'

8. Testing a Skill

Validation proves the file is well formed. It does not prove the skill works. A skill can fail by not loading when it should, or by loading and producing the wrong thing, and each of those needs its own test.

8.1. Testing Whether the Skill Triggers

Triggering is a property of the description, so we test it with queries rather than code. Keep a list of queries that should load the skill and a list that should not. Then run each query in a fresh session and record whether the agent used the skill.

[
  { "query": "we tagged v1.3.0 yesterday, can you write the release notes for the bookshelf repo", "should_trigger": true },
  { "query": "what changed since the last tag? need something for the blog post", "should_trigger": true },
  { "query": "draft a changelog entry for everything merged since v1.2.0", "should_trigger": true },
  { "query": "write a commit message for my staged changes", "should_trigger": false },
  { "query": "write a pull request description for this branch", "should_trigger": false },
  { "query": "bump the version in pyproject.toml to 1.3.0", "should_trigger": false }
]

Three runs per query is enough to see a pattern, because the same query doesn’t always resolve the same way. Two rules decide whether a query passes.

  • A should-trigger query passes when the skill loaded in at least two of the three runs.
  • A should-not query passes when the skill loaded in fewer than two.
QueryShould triggerLoaded inVerdict
we tagged v1.3.0 yesterday, can you write the release notesYes3 of 3Pass
what changed since the last tag?Yes3 of 3Pass
draft a changelog entry for everything merged since v1.2.0Yes2 of 3Pass
write a commit message for my staged changesNo0 of 3Pass
write a pull request description for this branchNo1 of 3Pass
bump the version in pyproject.toml to 1.3.0No0 of 3Pass

In Claude Code you can see the result in the transcript, where an activated skill appears as a tool call naming it. A failing should-trigger query means the description is missing a phrase that people use. A failing should-not query means the description is too broad.

8.2. Testing the Output Against a Baseline

The second test runs the task itself. We give the agent the prompt twice, once with the skill installed and once without it, then compare both runs against a list of assertions written in advance.

{
  "skill_name": "release-notes",
  "evals": [
    {
      "id": 1,
      "prompt": "Write the release notes for the bookshelf repo since the last tag.",
      "expected_output": "Markdown notes with Breaking changes, Added, Fixed and Changed sections and a list of commits that were skipped.",
      "files": [],
      "assertions": [
        "The notes contain a Breaking changes section with the commit that renames borrow to checkout",
        "The commit 'chore: bump pytest' does not appear in the notes",
        "The commit 'Update readme' is reported as skipped"
      ]
    }
  ]
}

Grading is manual, because the assertions are about meaning rather than exact text. Note that the baseline run does real work here. An assertion the agent passes without the skill isn’t testing the skill.

9. Common SKILL.md Mistakes

Nearly every mistake below comes back to a description that doesn’t describe, or a body that explains instead of instructing.

MistakeResultFix
A description such as “Helps with PDFs”The skill does not trigger, or it triggers on unrelated tasksState what the skill does, when to use it and when not to
The key use case at the end of a long descriptionTools shorten the description and cut the use casePut the main use case first
A name that differs from the folder name or has uppercase lettersThe tool rejects or ignores the skillRun a validator in continuous integration
Explaining what the agent already knowsThe body is long and the output does not improveKeep only project-specific facts, defaults and gotchas
A menu of equal optionsThe agent tries several and wastes stepsPick one default and mention alternatives in one line
A skill that covers a whole domainImprecise activation and conflicting stepsSplit it into one skill per task
Reference files without a loading conditionThe agent never reads them or reads all of themName the file and the condition in the body
A script that waits for inputThe run hangsTake every input from flags, and print a clear error when one is missing
Tool-specific frontmatter in a shared skillSome tools reject the fileKeep to the six open fields, or maintain one copy per tool
Never testing triggers or outputsThe skill works in the demo and fails laterKeep a query file and an evals file in the skill folder

10. Versioning and Sharing Skills

A skill that several people use is code, so it lives in the repository and goes through review like anything else there. Three habits keep a shared skill working.

  • Constrain the script dependencies. A PEP 723 block with pyyaml>=6,<7 keeps a breaking release out while still taking fixes. Pin an exact version only when a build has to be reproducible to the patch.
  • Record the version in metadata and change it when the steps change, so a reader can tell which version produced an old output.
  • Keep one copy of the folder and link the rest. Let’s say a team supports two agents. The skill lives in .agents/skills, and a symbolic link puts it in the Claude Code folder too.
ln -s ../../.agents/skills/release-notes .claude/skills/release-notes

The link points from the Claude Code folder to the real folder. So there is one file to edit, and both agents read it.

11. SKILL.md FAQs

These are the questions that come up once the first skill is written and the second one is being planned.

11.1. What Is a SKILL.md File?

SKILL.md is the entry file of an agent skill. It holds YAML frontmatter with a name and a description, then a Markdown body with the instructions. The agent reads the frontmatter at startup and the body only when the task matches.

11.2. Are Skills Just Prompts?

No. A prompt is sent once and then it is gone. A skill sits in the repository, loads itself when the task matches, and can carry scripts, references and templates that the agent runs or reads.

11.3. When Should We Create a Skill?

When the task repeats, takes five or more steps, and you’d otherwise retype the steps. A one-off task is a prompt, and a rule that applies to every task belongs in AGENTS.md.

11.4. Where Do We Put a SKILL.md File?

In a folder named after the skill, inside .claude/skills for Claude Code or .agents/skills for Codex and several others. The table in section 1.1 has the folder for each tool.

11.5. Does One Skill Work in Claude Code, Codex and Cursor?

Yes, as long as it uses the six open frontmatter fields and sits in a folder each tool reads. A tool-specific field such as disable-model-invocation is ignored by the tools that don’t know it, and rejected by the claude.ai upload.

11.6. How Long Should a SKILL.md Be?

Under 500 lines and 5,000 tokens for the body, per the specification. Most working skills are far shorter. Anything longer usually has reference material in it that belongs in references/.

11.7. Do We Need to Code to Write a Skill?

No. A skill with instructions alone is valid and useful. A script is worth adding when a step has exactly one correct answer and you don’t want the agent deriving it on every run.

11.8. Is SKILL.md the Same as AGENTS.md?

No. AGENTS.md is read at the start of every session and holds rules for the whole repository. A skill is read only when its description matches the task, and it can bring files with it.

12. Conclusion

A skill earns its place when a task repeats often enough that retyping the steps is the slow part. The description decides whether the skill ever runs. So it needs what the skill does, when to use it and when not to, with the main use case first, because tools shorten long descriptions.

The body decides whether the result is right. Numbered steps, a gotcha wherever the agent would guess, one example of the finished output, and a script wherever the answer is exact rather than a judgement. Keep the body under 500 lines and move the rest into references/.

Validate the file before you trust it, then test the two failure modes separately. A skill that doesn’t load is a description problem, and a skill that loads and produces the wrong thing is a body problem.

13. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.