hubagenticai

Tutorials Beginner · 6/8

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.

updated 2026-10-08 ⏱ 30 min

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.mdSKILL.md (a skill)MCP server
What it isProject instructions in one Markdown fileA folder with instructions and optional scriptsA program that gives the agent new tools
When it loadsEvery session, alwaysOnly when the task matches its descriptionTool list at startup; a tool runs when the model calls it
Good forFacts 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 costPaid every session, so keep it shortAbout one line until it is usedTool descriptions, plus each call’s results
ExampleRun tests with make testrelease-notes/SKILL.mdA 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:

  1. Discovery. At startup the agent reads only the name and description of each skill, about 100 tokens per skill.
  2. Activation. When your request matches a description, the agent reads the whole SKILL.md body.
  3. 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:

AgentProject foldersPersonal 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) and metadata (string keys mapped to string values) are optional. allowed-tools is 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 (or gemini skills list --all). The model activates a skill through an activate_skill tool 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.

newsletter

One practical agentic-AI guide in your inbox. No news, no hype.

Tutorials and decision frameworks as they ship. Unsubscribe anytime.

Quick Jump: