Skip to content
← ALL WRITING

2026-04-23 / 8 MIN READ

Building your first Claude skill: the five-file template

A hands-on tutorial for building your first Claude skill. Five files, one working skill, every decision explained without the theory.

The first Claude skill I built took me four hours. The second took about forty minutes. The difference was not the code. The difference was knowing which files matter and which ones are scaffolding.

By the end of this tutorial you will have a working skill on your machine that Claude Code auto-activates, runs in the main thread, and saves you the same forty minutes the next time you need that pattern. The skill is small on purpose. The point is the template, not the feature.

Skill skeleton/5 files
Entry point. The description is the single most important line.
---
name: pr-notes
description: Use when asked to write PR descriptions from a diff. Extracts the why, the what, and the risk from staged changes.
---

# PR Notes

## When to use
- User says "write the PR description"
- User asks for a "PR summary" or "commit message"

## How to use
1. Run `git diff --staged` to get the change set.
2. Extract the why (from commit messages), the what (files changed), and the risk (tests touched).
3. Format using scripts/format.py.

Prerequisites

You need:

  • Claude Code installed (CLI or the VS Code extension). Any version from late 2024 forward supports skills.
  • A shell you are comfortable with (bash or zsh).
  • Python 3.10 or newer, or Node 18 or newer. I will show Python; the pattern works the same in Node.

You do not need:

  • An Anthropic API key. Skills run via your Claude Code session.
  • An MCP server. Skills are simpler than MCP. If you are choosing between them, this decision log covers when each one wins.
  • An existing project. We will build the skill standalone and use it locally.

What we will build

A skill that writes PR descriptions from a staged diff. When you say "write the PR description," the skill fires, reads git diff --staged, and produces a structured markdown block with a why, a what, and a risk section.

The skill is deliberately boring. That is the point. Boring skills ship and get used.

Where skills live

Skills live in one of two places:

  • User-level: ~/.claude/skills/<skill-name>/. Available across all projects.
  • Project-level: .claude/skills/<skill-name>/. Scoped to the current repo.

For a personal tool, user-level is fine. For a skill tied to a specific project's conventions, keep it project-level. We will use user-level for this tutorial.

Step 1: scaffold the directory

mkdir -p ~/.claude/skills/pr-notes/{scripts,references,tests}
cd ~/.claude/skills/pr-notes

The three subdirectories map to what each holds:

  • scripts/: helpers the model shells out to.
  • references/: extended docs the model loads on demand.
  • tests/: a smoke test so you know the helper still works.

You can skip any of these if you do not need them. A minimum-viable skill is a single SKILL.md.

Step 2: write SKILL.md

This is the file that does 80 percent of the work. The model reads SKILL.md on every session start. The description field in the frontmatter is matched against every user message to decide whether to activate the skill.

---
name: pr-notes
description: Use when asked to write PR descriptions from a diff. Extracts the why, the what, and the risk from staged changes.
---

# PR Notes

## When to use
- User says "write the PR description"
- User asks for a "PR summary" or "commit message"
- User wants a structured summary of staged changes

## How to use
1. Run `git diff --staged` to get the change set.
2. Extract the why (from commit messages), the what (files changed), and the risk (tests touched).
3. Run `scripts/format.py` to produce the final markdown.

## Style
See `references/style.md` for the exact formatting rules.

## Examples
See `references/examples.md` for two real PRs formatted correctly.

The single most important line: description. The model's skill-matching runs against this string. Be specific. "Use when asked to write PR descriptions" will match the right work. "A skill for PRs" will not.

The body of SKILL.md tells the model how to execute. Keep it tight. Offload longer content (style guides, example library) to references/ files that the model loads only when needed.

Step 3: write the helper script

# scripts/format.py
#!/usr/bin/env python3
"""Format PR notes from a structured dict into markdown."""
import json, sys

def format_pr(data):
    md = f"## Why\n\n{data['why']}\n\n"
    md += "## What\n\n"
    for item in data['changes']:
        md += f"- {item}\n"
    md += f"\n## Risk\n\n{data['risk']}\n"
    return md

if __name__ == "__main__":
    data = json.load(sys.stdin)
    print(format_pr(data))

Make it executable:

chmod +x scripts/format.py

This is the deterministic half of the skill. The model extracts the content (that is LLM work). The script formats the content (that is deterministic work). Keeping these separate is the single biggest lesson from my first few skills. When the LLM does formatting, you get inconsistent output. When a script does formatting, you get the same output every time.

Step 4: write the reference files

# references/style.md

- Why: one sentence, present tense.
- What: bullet list, files changed plus intent.
- Risk: call out tests modified, breaking changes, migration needs.

No em dashes. No staccato negation lists.
# references/examples.md

## Example 1: A bug fix

### Why
The checkout total was off by the shipping discount when coupons were stacked.

### What
- src/lib/cart.ts - applied discount order fix
- src/lib/cart.test.ts - added stacked coupon test

### Risk
Touches cart totals. Regression risk is low since the old behavior was visibly wrong.

These files do not load on every session start. The model reads them only when SKILL.md points to them. This keeps the always-loaded footprint tiny and lets you pack more context behind the skill without paying for it on every session.

Step 5: write a smoke test

# tests/smoke.sh
#!/usr/bin/env bash
set -euo pipefail
echo '{"why":"x","changes":["a"],"risk":"low"}' | python3 scripts/format.py > /tmp/out.md
grep -q "## Why" /tmp/out.md && echo "PASS" || { echo "FAIL"; exit 1; }

Make it executable and run it:

chmod +x tests/smoke.sh
./tests/smoke.sh

The smoke test exists so you know the helper script works the next time you touch the skill six months from now. You will not remember. The test will.

Step 6: use the skill

Restart Claude Code (or start a new session). Make some changes to a repo, stage them, and ask:

Write the PR description for what I've got staged.

Claude Code will pattern-match "PR description" against your skill's description field, auto-activate pr-notes, run git diff --staged, extract the why/what/risk, pipe the structured data through scripts/format.py, and return formatted markdown.

First run will take 20-30 seconds. Subsequent runs in the same session are faster because the skill is already loaded.

Common mistakes

These are the ones I made on my first three skills:

Vague description field. "A skill for pull requests" never activates. "Use when asked to write PR descriptions from a diff" activates reliably. The more specific, the better.

LLM doing formatting. If the model is deciding whether to use two spaces or four, you have designed the skill wrong. Push deterministic work into a helper script.

Too much in SKILL.md. If SKILL.md is over 200 lines, split the extra into references/ files. SKILL.md loads every session; references load on demand.

No smoke test. You will come back in three months and not remember how the skill works. The smoke test is your note to future you.

Forgetting to chmod +x. Helper scripts need to be executable. I still forget this one.

What to try next

The minimum-viable skill you just built is the template I start every new skill from. To level it up:

  • Add a second helper script for a related task. Skills can contain multiple scripts that SKILL.md points to conditionally.
  • Add a CLAUDE.md in a project where you plan to use the skill, with a note about when to invoke it. The model reads CLAUDE.md alongside SKILL.md on project-scoped skills.
  • Move the skill to .claude/skills/ in a specific project if you want it scoped. Drop it back to ~/.claude/skills/ if you want it everywhere.

If you find yourself writing the same skill pattern more than twice, the Claude Code skills pack bundles the skills I use daily for DTC and ecommerce work, ready to drop into your ~/.claude/skills/ directory.

For the broader decision about when a skill is even the right choice, see MCP vs skills vs tools. For the hub view of the full handbook, the agent engineering handbook for operators indexes every pattern.

FAQ

How does Claude decide whether to activate a skill?

It pattern-matches the user's message against the description field in each skill's SKILL.md. Specific descriptions match reliably; vague ones do not. If two skills could both activate, Claude picks based on specificity and recency.

Can a skill write files?

Yes, via helper scripts. The script runs as a subprocess with the same filesystem access the agent has. Be careful about scripts that write outside the project directory.

Do skills work offline?

The skill infrastructure is local. The LLM that activates them still needs a Claude Code session with a model connection. If Claude Code works, skills work.

Can I version-control my skills?

Yes. Project-scoped skills (.claude/skills/) commit with the repo. User-scoped skills (~/.claude/skills/) you manage yourself. Mine live in a separate git repo that I symlink into ~/.claude/skills/.

What is the difference between a skill and a sub-agent?

A skill is a pattern loaded into the main thread. A sub-agent is a separate context. Skills are cheaper and persist across tasks. Sub-agents are for isolated heavy work. The sub-agent post covers the decision.

// related

Claude Code Skills Pack

If you want to go deeper on agentic builds, this pack covers the patterns I use every day. File ownership, parallel agents, tool contracts.

>View the pack