How to write Agent Skills (SKILL.md) that work across coding agents
A skill is a folder with a SKILL.md file that a coding agent loads only when it needs it. Build a working release-notes skill with a bundled script, put it where Claude Code, Codex, Copilot, Cursor and Gemini CLI can all find it, and fix it when it does not trigger.
Every coding agent keeps forgetting how your team does the same small jobs: how to write release notes, how to name a migration, which checklist to run before a deploy. You could paste the instructions into every chat, or put them all in one giant instructions file that the agent reads every time. Agent Skills are the better option. A skill is a folder of instructions (and, if needed, scripts) that the agent loads only when the task calls for it.
The format began at Anthropic and is now an open specification at agentskills.io. As of October 2026 its client list includes Claude Code, OpenAI Codex, GitHub Copilot, VS Code, Cursor, Gemini CLI, goose, OpenHands, Junie and dozens more, so you can write a skill once and use it in many tools.
Step 1: Know where a skill fits (AGENTS.md vs. SKILL.md vs. MCP)
Coding agents give you three main ways to extend them. Beginners often mix them up, so here they are side by side:
| AGENTS.md | SKILL.md (a skill) | MCP server | |
|---|---|---|---|
| What it is | Project instructions in one Markdown file | A folder with instructions and optional scripts | A program that gives the agent new tools |
| When it loads | Every session, always | Only when the task matches its description | Tool list at startup; a tool runs when the model calls it |
| Good for | Facts that are always true: build commands, code style, “never edit /vendor” | Procedures used sometimes: “how we write release notes” | Actions in other systems: query a database, open a ticket |
| Context cost | Paid every session, so keep it short | About one line until it is used | Tool descriptions, plus each call’s results |
| Example | Run tests with make test | release-notes/SKILL.md | A GitHub or Postgres MCP server |
Here’s a simple test. If you’d tell a new teammate on their first day, put it in AGENTS.md. If it’s a how-to they’d look up when they need it, make it a skill. If the agent needs to do something in another system, use an MCP tool. The skills vs. tools vs. hooks vs. subagents comparison covers the rest of the options.
AGENTS.md is an open format too (it’s a project of the Agentic AI
Foundation), and most coding agents read it. While testing for this guide,
Claude Code 2.1.287 read a repo’s AGENTS.md even though the repo had no
CLAUDE.md.
Step 2: Understand how a skill loads (why the description matters)
Skills use what the spec calls progressive disclosure, which happens in three stages:
- Discovery. At startup the agent reads only the
nameanddescriptionof each skill, about 100 tokens per skill. - Activation. When your request matches a description, the agent reads
the whole
SKILL.mdbody. - Execution. The agent follows the instructions, and reads the skill’s other files or runs its scripts only when the instructions call for them.
So the description is a trigger, not a summary. It has to say what the skill does and when to use it, in the words your users actually type. Nearly every “my skill never fires” problem comes down to the description.
Step 3: Create the skill folder in a portable place
Tools look for skills in different folders. As of October 2026, their docs list these:
| Agent | Project folders | Personal folders |
|---|---|---|
| Claude Code | .claude/skills/ | ~/.claude/skills/ |
| OpenAI Codex | .agents/skills/ (from the current directory up to the repo root) | ~/.agents/skills/ |
| GitHub Copilot / VS Code | .github/skills/, .claude/skills/, .agents/skills/ | ~/.copilot/skills/, ~/.claude/skills/, ~/.agents/skills/ |
| Cursor | .agents/skills/, .cursor/skills/ (also reads .claude/skills/ and .codex/skills/) | ~/.agents/skills/, ~/.cursor/skills/ (plus the Claude and Codex ones) |
| Gemini CLI | .gemini/skills/, .agents/skills/ | ~/.gemini/skills/, ~/.agents/skills/ |
Most tools read .agents/skills/, but Claude Code doesn’t list it. A simple
fix is to keep one real copy in .agents/skills/ and add a
symlink from .claude/skills/. Claude Code’s docs say a skill folder can
be a symlink, and we tested that it works.
cd your-repo
mkdir -p .agents/skills/release-notes/scripts
mkdir -p .claude/skills
ln -s ../../.agents/skills/release-notes .claude/skills/release-notes
Other tools support skills too, with their own folders. Google’s Antigravity is one, and published guides disagree about its path, so check your tool’s own skills page (the agentskills.io client list links to each one).
Step 4: Write SKILL.md
Create .agents/skills/release-notes/SKILL.md:
---
name: release-notes
description: Drafts release notes and changelog entries from the git history since the last tag. Use when the user asks for release notes, a changelog, "what changed since the last release", or is preparing to tag a version.
license: MIT
compatibility: Requires git and Python 3.9+
metadata:
author: your-team
version: "1.0"
---
# Release notes
## Steps
1. Run the bundled script `scripts/changelog.py` (the path is relative to
this skill's folder) from the root of the user's repository:
```bash
python3 <this-skill-folder>/scripts/changelog.py
```
It prints Markdown grouped into Breaking changes, Features, Fixes and
Other, covering every commit since the most recent tag (or all commits
if there is no tag). To choose the starting point yourself, pass a ref:
`--since v1.2.0`.
2. Read the output and rewrite it for humans:
- Lead with the change users will notice most.
- Merge commits that describe the same change into one bullet.
- Drop pure chores (CI tweaks, typo fixes) unless the user asks for everything.
- Keep the commit hashes in brackets so reviewers can trace each line.
3. If the user wants it saved, prepend the result to `CHANGELOG.md` under a
heading with the new version and today's date. Never create a git tag
or push unless the user explicitly asks.
## Rules
- Do not invent changes that are not in the script output.
- If the script prints "No commits found", say so instead of writing notes.
The frontmatter rules from the specification are:
name(required): 1–64 characters, lowercase letters, digits and hyphens only. It can’t start or end with a hyphen or contain--, and it must match the folder name.description(required): 1–1024 characters, saying what the skill does and when to use it.license,compatibility(up to 500 characters) andmetadata(string keys mapped to string values) are optional.allowed-toolsis optional and experimental, and support for it varies by tool.
Individual tools add their own extra fields. For example, Claude Code has
disable-model-invocation: true, which stops the model from loading the
skill on its own so it runs only when you type /release-notes. Cursor
honours the same field. Use extra fields when you need them, but a skill
that uses only the spec fields will work in every tool.
Keep the body under about 500 lines, as the spec recommends. Put long
reference material in a references/ folder and link to it with a
relative path, so it loads only when it’s needed.
Step 5: Add the bundled script
Create .agents/skills/release-notes/scripts/changelog.py:
#!/usr/bin/env python3
"""Print Markdown release notes from git history (stdlib only)."""
import argparse
import re
import subprocess
import sys
GROUPS = {"feat": "Features", "fix": "Fixes"}
PATTERN = re.compile(r"^(?P<type>\w+)(\([^)]*\))?(?P<bang>!)?:\s*(?P<msg>.+)$")
def git(*args):
result = subprocess.run(["git", *args], capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(result.stderr.strip() or "git failed")
return result.stdout.strip()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--since", help="tag or commit to start from (default: latest tag)")
args = parser.parse_args()
try:
git("rev-parse", "--is-inside-work-tree")
except (RuntimeError, FileNotFoundError):
sys.exit("error: run this inside a git repository (and make sure git is installed)")
since = args.since
if since is None:
try:
since = git("describe", "--tags", "--abbrev=0")
except RuntimeError:
since = None # no tags yet: use the whole history
rev_range = f"{since}..HEAD" if since else "HEAD"
try:
log = git("log", rev_range, "--no-merges", "--pretty=format:%h%x09%s")
except RuntimeError as err:
sys.exit(f"error: {err}")
if not log:
print(f"No commits found since {since or 'the beginning'}.")
return
sections = {"Breaking changes": [], "Features": [], "Fixes": [], "Other": []}
for line in log.splitlines():
sha, subject = line.split("\t", 1)
match = PATTERN.match(subject)
if match:
kind = GROUPS.get(match["type"].lower(), "Other")
text = match["msg"]
if match["bang"]:
kind = "Breaking changes"
else:
kind, text = "Other", subject
sections[kind].append(f"- {text} [{sha}]")
print(f"## Changes since {since or 'the first commit'}\n")
for title, items in sections.items():
if items:
print(f"### {title}\n")
print("\n".join(items) + "\n")
if __name__ == "__main__":
main()
Why use a script instead of letting the agent run git log itself? A script
gives the same output every time, takes only a few tokens to run, and fails
with a clear error message. The agent then does what it’s good at:
rewriting the raw list for people to read.
Test the script on its own before you hand it to an agent. Here’s a throwaway repo with a few Conventional Commits:
chmod +x .agents/skills/release-notes/scripts/changelog.py
git init -q demo && cd demo
git commit -q --allow-empty -m "chore: initial commit" && git tag v0.1.0
git commit -q --allow-empty -m "feat: add CSV export"
git commit -q --allow-empty -m "fix(api): handle empty payloads"
git commit -q --allow-empty -m "feat!: drop Python 3.8 support"
git commit -q --allow-empty -m "update docs"
python3 ../.agents/skills/release-notes/scripts/changelog.py
The output (your hashes will differ):
## Changes since v0.1.0
### Breaking changes
- drop Python 3.8 support [1aefe33]
### Features
- add CSV export [267ad04]
### Fixes
- handle empty payloads [e600025]
### Other
- update docs [1a6ffb9]
We also ran the edge cases. With no commits since the tag it prints
No commits found since v0.2.0., outside a git repo it exits with
error: run this inside a git repository, and given a bad --since ref it
passes git’s own error through and exits 1.
Step 6: Validate the frontmatter
The spec’s authors publish a reference validator, skills-ref. Its README
says it’s for demonstration, not production, but it’s a good lint. You
can run it straight from the repository with uvx:
uvx --from "git+https://github.com/agentskills/agentskills#subdirectory=skills-ref" \
skills-ref validate .agents/skills/release-notes
Valid skill: .agents/skills/release-notes
skills-ref to-prompt <skill-dir> prints the <available_skills> block an
agent would put in its system prompt. It’s a quick way to see exactly what
the model reads at the discovery stage.
Step 7: Check that your agent finds and uses it
Claude Code (the one we tested). Run this inside the repo:
claude -p "Which skills are available to you? List only names containing 'release'."
It listed release-notes with our description, through the symlink. Next
we asked a question that never named the skill:
claude -p "What changed since the last release? Draft the notes, don't save anything." \
--allowedTools "Skill" "Bash(python3:*)" "Bash(git:*)" "Read"
Claude Code loaded the skill by itself (a Skill tool call for
release-notes), ran scripts/changelog.py, left the docs-only commit out
as the instructions said, and saved nothing. In an interactive session you
can also type /release-notes, or open /skills to see what’s loaded.
Other agents. These are the documented ways to call a skill explicitly. We haven’t run them for this guide:
- Codex: mention it with
$release-notes, or pick it from/skills. - Cursor: type
/in Agent chat and search for the skill name. - Gemini CLI: run
/skills list(orgemini skills list --all). The model activates a skill through anactivate_skilltool and asks you to confirm first.
All of them also choose skills automatically by matching the description, which is why Step 2 matters so much.
Troubleshooting
The skill never triggers on its own
Almost always, the description is the problem. A description like
“Helps with releases” gives the model nothing to match. Say what it
does and when to use it, using the words people type: “release notes”,
“changelog”, “what changed since the last release”. Then check that
explicit calls work (/release-notes, $release-notes). If explicit
calls work but automatic ones don’t, the description is the problem. If
explicit calls also fail, the agent hasn’t discovered the skill at all
(see the next item).
The agent says the skill doesn’t exist
Check the folder layout against the table in Step 3. The file must be
exactly SKILL.md, inside a folder named after the skill, inside a
skills folder: .claude/skills/release-notes/SKILL.md, not
.claude/skills/SKILL.md. Claude Code doesn’t list .agents/skills/,
so it needs the symlink (or a copy) under .claude/skills/. On Windows,
creating symlinks may need Developer Mode, so copying the folder is the
easy fallback. If you created a brand-new skills folder during a Claude
Code session, run /reload-skills.
Validator: “Directory name ‘release-notes2’ must match skill name ‘release-notes’”
The name field and the folder name must be identical. Rename one of
them.
Validator: “Skill name ‘Release_Notes’ must be lowercase … contains invalid characters”
Names allow only a-z, 0-9 and single hyphens. Use release-notes.
”Invalid YAML in frontmatter: mapping values are not allowed here”
There’s an unquoted colon inside a value, for example
description: Use when: the user asks for notes. Put quotes around the
whole value (description: "Use when: the user asks for notes") or
rephrase it without the colon. Also make sure the frontmatter begins
with --- on the very first line of the file.
The script fails with “error: run this inside a git repository”
The agent ran the script from the wrong directory. The SKILL.md tells it to run from the repository root. If this keeps happening, add a line with the exact command for your layout.
Where to go next
Skills hold procedures. To give an agent new actions, build a tool: start with your first MCP server. If you’re new to Claude Code, Claude Code from zero covers the rest of its workflow. The SKILL.md template has a smaller starter you can copy.
Was this guide useful?
Thanks — noted. It shapes what gets written next.
newsletter
One practical agentic-AI guide in your inbox. No news, no hype.
Tutorials and decision frameworks as they ship. Unsubscribe anytime.