Recipe · Basic · 10 min
Create your first Claude Code skill, step by step
Build a project skill in .claude/skills with SKILL.md, a helper script and a description Claude recognizes. Example: release notes from git log.
Tested with Claude Code 2.1.316 · reviewed
The problem
Every time you cut a release you paste the same instructions into Claude: "look at the commits since the last tag, group them into features and fixes, don't make anything up". When those instructions live in your head or in the chat, they get lost and everyone on the team gets a different result. A skill stores that procedure in the repository, so you can run it with /name or let Claude load it on its own when it's needed.
The solution
We'll build the version-notes skill, which writes Markdown release notes from git log.
1. Create the skill folder
Project skills live in .claude/skills/<name>/SKILL.md at the root of the repository. Personal skills go in ~/.claude/skills/<name>/SKILL.md and work in all your projects.
mkdir -p .claude/skills/version-notes/scripts
2. Write SKILL.md
Save this as .claude/skills/version-notes/SKILL.md. The block between --- lines is the frontmatter, and it must start on the first line of the file.
---
name: version-notes
description: Generates Markdown release notes from the git commits since the last tag. Use when asked for release notes, a changelog, or "what changed since the last version".
argument-hint: "[from-tag]"
allowed-tools: Bash(bash *version-notes/scripts/commits.sh*) Bash(git log *) Bash(git describe *)
---
# Release notes
## Commits since the last version
!`bash "${CLAUDE_SKILL_DIR}/scripts/commits.sh" $ARGUMENTS`
## Instructions
1. Group the commits above by prefix: `feat` → **New**, `fix` → **Fixes**, `docs` → **Documentation**, anything else → **Other changes**.
2. Rewrite each commit as a short, user-facing sentence (no hash or prefix), in the language of the commits.
3. Skip empty sections. Don't invent changes that aren't in the list.
4. Return only the Markdown, starting with `## Changes since <tag>`. Follow the format in [template.md](template.md).
What each field does:
| Field | What it's for |
|---|---|
name | The command in the / menu. Defaults to the folder name. |
description | What the skill does and when to use it. Claude reads it to decide whether to load the skill. |
argument-hint | The hint shown during autocomplete, for example [from-tag]. |
allowed-tools | Tools that skip the permission prompt during the turn that invokes the skill. |
The ` !command line runs **before** Claude reads the skill, and its output replaces the line. That way Claude gets the real commits instead of having to look for them. ${CLAUDE_SKILL_DIR} is the skill's folder and $ARGUMENTS is whatever you type after /version-notes`.
Other optional fields in the official guide: when_to_use (more trigger phrases), arguments (named arguments), disable-model-invocation: true (only you can invoke it), user-invocable: false (only Claude can invoke it), disallowed-tools, model, effort, context: fork with agent (run it in a subagent), hooks, paths (activate it only for certain files) and shell. All of them are optional; Claude Code silently ignores field names it doesn't recognize.
3. Add a helper script
Scripts are executed, not loaded into context. Save this as .claude/skills/version-notes/scripts/commits.sh:
#!/usr/bin/env bash
# Lists commits since the given tag (or the latest one). With no tags, lists the whole history.
from="${1:-$(git describe --tags --abbrev=0 2>/dev/null)}"
if [ -n "$from" ]; then
echo "Starting tag: $from"
git log --no-merges --pretty='- %s (%h)' "$from"..HEAD
else
echo "Starting tag: (none, full history)"
git log --no-merges --pretty='- %s (%h)'
fi
chmod +x .claude/skills/version-notes/scripts/commits.sh
4. Add a reference file
Supporting files keep SKILL.md short: Claude reads them only when it needs them, because SKILL.md links to them. Save this as .claude/skills/version-notes/template.md:
## Changes since v1.2.0
### New
- Export your favorites to CSV.
### Fixes
- Logging out no longer leaves the session open.
You end up with this layout:
.claude/skills/version-notes/
├── SKILL.md
├── template.md
└── scripts/
└── commits.sh
5. Invoke it
Inside Claude Code type /version-notes or, to start from another tag or commit, /version-notes v1.0.0. It also works outside a session in non-interactive mode:
claude -p "/version-notes"
No restart needed: Claude Code watches the skill folders and applies changes to the open session. If you created the .claude/skills/ folder while the session was already open, run /reload-skills.
6. Write a description that makes Claude use it
Claude only sees each skill's name and description until it decides to load it. To get it picked:
- Start with what it does: "Generates Markdown release notes from the git commits…".
- Say when to use it, in the words people actually use: "release notes, a changelog, what changed since the last version".
- Put the important part first:
descriptionandwhen_to_usetogether are cut to 1,536 characters in the listing. - Avoid
<and>: they show as<and>. Use square brackets, as in[from-tag].
With that description, the prompt "what changed since the last version? give me the changelog" made Claude invoke the skill on its own, without typing /version-notes.
Check it
Tested with Claude Code 2.1.316 in a test repository with the tag v0.1.0 and two later commits (feat: export favorites to CSV and docs: explain installation in the README):
claude -p "/version-notes"
Actual output:
## Changes since v0.1.0
### New
- Export your favorites to CSV.
### Documentation
- The README now explains how to install the app.
With an argument (claude -p "/version-notes <first-commit-hash>") the fix commit and the Fixes section show up too. The exact wording changes a little on each run; the commits included don't.
Why it works
At startup Claude Code loads only the name and description of each skill. The body of SKILL.md enters the context when you invoke it with /version-notes or when Claude decides the description matches your request, and it stays in the conversation from then on. Before handing it to Claude, Claude Code runs the ` !… ` lines and puts their output in their place. That's why a skill costs no context until you use it.
Skill, CLAUDE.md and subagent, according to the official documentation:
| When it loads | What it's for | |
|---|---|---|
CLAUDE.md | Always, at session start | Facts and standing project conventions |
| Skill | Description always; body only when invoked | Procedures, checklists, reference material |
| Subagent | In its own separate context | Delegated work with its own tools and instructions |
The guide's rule of thumb: create a skill when you catch yourself pasting the same instructions into the chat, or when a CLAUDE.md section has turned into a procedure rather than a fact. If you want the skill to run in isolation, context: fork runs it in a subagent, which doesn't see your conversation. Docs: Skills and Subagents.
Common mistakes
- You named the skill
release-notesandclaude -p "/release-notes"prints nothing →/release-notesis a built-in Claude Code command, and in-pmode the built-in ran instead of your skill (the guide only says your skill replaces it in a local terminal session) → use a name that doesn't match any built-in command, such asversion-notes. claude -p "/version-notes"exits without printing anything → the `!…line needed permission and non-interactive mode has no one to ask, so the invocation is cancelled → add the command toallowed-tools, for exampleBash(bash version-notes/scripts/commits.sh). Withclaude --debug-file log.txtyou'll seeShell command permission check failed`.- In
claude -p, Claude tries to use the skill on its own, says it "failed to load" and does the work by hand → non-interactive mode had no permission for the Skill tool → add--allowedTools "Skill(version-notes)"or the ruleSkill(version-notes)to theallowlist in your permissions. - Frontmatter fields have no effect → the
---isn't on the first line, the YAML is invalid, or a field name is misspelled (disable-model-invocationuses hyphens) → fix it;claude plugin validate .claude/skillsfinds SKILL.md files with broken frontmatter. - Claude never uses it unless you type
/name→ the description is vague or it hasdisable-model-invocation: true→ include the words you'd use when asking for it.
FAQ
What's the difference between a skill and a command in .claude/commands?
Custom commands were merged into skills: .claude/commands/deploy.md and .claude/skills/deploy/SKILL.md both create the same /deploy. A skill adds a folder for supporting files and automatic loading by Claude; if both share a name, the skill wins.
Do I need to restart Claude Code after editing a skill?
No. Claude Code watches the skill folders and applies changes to the open session. You only need /reload-skills when you create a top-level skills folder for the first time.
How do I stop Claude from running it on its own?
Add disable-model-invocation: true to the frontmatter. Then it only runs when you type /version-notes, which is a good idea for skills with side effects, such as deploying or publishing.
Can I share it with my team?
Yes. Commit the .claude/skills/ folder to the repository, and anyone who opens Claude Code in that project gets /version-notes.
How long should SKILL.md be?
The official guide recommends under 500 lines. Put the details in supporting files linked from SKILL.md, which Claude reads only when it needs them.