Up to date as of April 2026.
I run a small self-hosted Mastodon instance. Upgrading it is the same twenty minutes every time: read the release notes, back up the database, bump three image tags, restart, verify, prune the old images. None of it is hard and all of it is forgettable, which is the worst combination — the step you skip on a patch release is the backup, and patch releases are exactly when you find out you needed it.
It’s the only procedure I’ve bothered to write down as a skill. It’s also the one I’d point at to explain what skills are for.
Playbooks, not standing rules
Skills are how I “teach” Claude Code repeatable workflows without bloating CLAUDE.md.
A skill is a directory with a SKILL.md file — frontmatter for metadata, Markdown for the instructions. Claude can load it automatically when the context fits, or you run it directly as a slash command.
The way I think about it: CLAUDE.md is the standing rules (always loaded, always applies), skills are the playbooks (loaded on demand, for specific actions or workflows).
Where people get confused:
- CLAUDE.md / rules — always on. Right for stable constraints: commands, conventions, architecture decisions.
- Skills — on demand. Right for actions you do repeatedly: deploy, review, commit. Or reference material that’s only relevant sometimes.
.claude/commands/— the old format. Still works, but skills replaced it. Both.claude/commands/deploy.mdand.claude/skills/deploy/SKILL.mdcreate/deploy. Skills are more flexible because you can bundle supporting files alongside the instructions.
Where to store them
| Location | Scope |
|---|---|
~/.claude/skills/<skill-name>/SKILL.md |
All your projects (personal) |
.claude/skills/<skill-name>/SKILL.md |
This project only (in git) |
Plugin: <plugin>/skills/<skill-name>/SKILL.md |
Where the plugin is enabled |
On name conflicts: enterprise > personal > project. Plugin skills use a plugin-name:skill-name namespace, so they don’t collide.
If you’re working alone, start with personal skills. If a workflow clearly belongs to one repo, move it into .claude/skills/ and commit it.
Minimal skill
Deliberately trivial, so the only thing being tested is the mechanism: does Claude find it, and does it follow it.
mkdir -p ~/.claude/skills/explain-code
~/.claude/skills/explain-code/SKILL.md:
---
name: explain-code
description: Explains code with visual diagrams and analogies. Use when explaining how code works, teaching about a codebase, or when the user asks "how does this work?"
---
When explaining code, always:
1. **Start with an analogy** — compare to something from everyday life
2. **Draw a diagram** — ASCII art for structure or flow
3. **Walk through step-by-step** — what happens at each stage
4. **Highlight a gotcha** — common mistake or misconception
Test it two ways:
- Let Claude pick it up automatically: “How does this code work?”
- Invoke explicitly:
/explain-code src/auth/login.ts
That’s the whole structure: name becomes the slash command, description is what Claude reads to decide whether to load it on its own, and everything under the frontmatter is the instructions.
All frontmatter fields
You do not need most of these most of the time. The Mastodon skill from the opener is 156 lines and uses exactly two fields — name and description — and I’ve never wanted a third.
| Field | Description |
|---|---|
name |
Display name. If omitted, uses the directory name. Lowercase letters, numbers, and hyphens only. Max 64 chars. |
description |
What it does and when to use it. Recommended. Descriptions longer than ~250 chars are truncated in the listing, so front-load the use case. |
argument-hint |
Autocomplete hint for expected arguments. Example: "[issue-number]" |
disable-model-invocation |
true to prevent Claude from auto-loading it. You run it manually as /name. |
user-invocable |
false to hide it from the / menu (Claude can still use it). |
allowed-tools |
Tools Claude can use without asking permission while the skill is active. Accepts a space-separated string or a YAML list. |
model |
Override the model when the skill is active. |
effort |
Override effort level while the skill is active: low, medium, high, xhigh, max. The top tiers depend on the model you’re running. |
context |
fork to run the skill in a forked subagent context. |
agent |
Which subagent type to use when context: fork is set. |
hooks |
Hooks scoped to this skill’s lifecycle. |
paths |
Glob patterns limiting when Claude auto-loads the skill (same format as path-scoped rules). |
shell |
Shell for inline ! command blocks (bash or powershell, Windows-only toggle required). |
Three types of content
I think about skills in three buckets:
1. Reference — knowledge Claude applies
Loaded inline, Claude uses it alongside the conversation:
---
name: api-conventions
description: API design patterns for this codebase
---
When writing API endpoints:
- RESTful naming conventions
- Consistent error format: { error, code, message }
- Input validation on all endpoints
- JSDoc comments for all public routes
2. Task — step-by-step instruction for a specific action
For actions you want to control yourself, add disable-model-invocation: true:
---
name: deploy
description: Deploy the application to production
disable-model-invocation: true
context: fork
---
Deploy to production:
1. Run test suite — `bun test`
2. Build — `bun run build`
3. Push to deployment target
4. Verify deployment succeeded
3. Context — background knowledge for Claude only
Hidden from menu, Claude uses when needed:
---
name: legacy-system-context
description: Context about the legacy billing system. Load when working with src/billing/legacy/
user-invocable: false
paths:
- "src/billing/legacy/**"
---
# Legacy Billing System
This system was written in 2015 and uses...
Never touch legacy-core.js directly...
Context skills are the most underrated type. A skill that silently loads background knowledge about a gnarly part of your codebase — no slash command needed, just relevant file paths in paths: — quietly makes Claude better every time you touch that code. No one ever notices it’s there. That’s the point.
Arguments
Arguments are where skills stop being static templates and start feeling like tools.
---
name: fix-issue
description: Fix a GitHub issue by number
disable-model-invocation: true
argument-hint: "[issue-number]"
---
Fix GitHub issue #$ARGUMENTS:
1. Read the issue description
2. Find relevant code
3. Implement the fix
4. Write tests
5. Create a commit
Invocation: /fix-issue 123
$ARGUMENTS is everything after the command name. If you want the parts separately, they’re positional:
---
name: migrate-component
---
Migrate the $0 component from $1 to $2.
# /migrate-component SearchBar React Vue
# $0 = SearchBar, $1 = React, $2 = Vue
Dynamic content via bash
The ! prefix executes a command and inserts the result before Claude sees the prompt:
---
name: pr-summary
description: Summarize the current pull request
allowed-tools: Bash(gh *)
context: fork
agent: Explore
---
## Pull Request Context
- Diff: !`gh pr diff`
- Comments: !`gh pr view --comments`
- Changed files: !`gh pr diff --name-only`
## Task
Summarize this pull request: what changed, why, and what to review carefully.
Commands execute before sending to Claude, which is why this pattern is useful for things like PR review, release notes, or incident summaries.
Path-specific skills
Load only when Claude is working with matching files:
---
name: react-patterns
description: React component patterns for this codebase
paths:
- "src/components/**/*.tsx"
- "src/pages/**/*.tsx"
---
When writing React components:
- Functional components only, no class components
- Custom hooks in src/hooks/
- Styles via CSS modules
Running in a subagent (context: fork)
The skill executes in an isolated context, so the main conversation stays cleaner:
---
name: deep-research
description: Research a topic thoroughly in the codebase
context: fork
agent: Explore
---
Research $ARGUMENTS thoroughly:
1. Find relevant files using Glob and Grep
2. Read and analyze the code
3. Return a summary with specific file references
Pick agent based on what you want (read-only research vs implementation), or point at a custom subagent from .claude/agents/.
Folder with additional files
If a skill starts turning into a wall of Markdown, break it up. SKILL.md should stay readable:
.claude/skills/deploy/
├── SKILL.md # main instructions + navigation
├── checklist.md # detailed checklist
├── rollback-guide.md # instructions in case of problems
└── scripts/
└── health-check.sh # verification script
In SKILL.md reference files explicitly:
---
name: deploy
description: Deploy to production with full checklist
---
Follow the deployment checklist in [checklist.md](checklist.md).
If something goes wrong, see [rollback-guide.md](rollback-guide.md).
After deployment run: !`./scripts/health-check.sh`
Keep SKILL.md under 500 lines. Details go in separate files.
I’ve never hit that ceiling. The Mastodon one is the longest thing I’ve written and it’s a third of it — which makes me think the 500-line skill is usually two skills that haven’t been separated yet.
Invocation control
Worth getting right early, because it decides how “pushy” the skill feels in everyday use.
| Frontmatter | You invoke | Claude invokes | In context |
|---|---|---|---|
| (default) | yes | yes | Description always, content on invocation |
disable-model-invocation: true |
yes | no | Not loaded automatically |
user-invocable: false |
no | yes | Description in context, hidden from menu |
The rule I use: anything with a side effect outside the session — /deploy, /commit, /send-message — gets disable-model-invocation: true, because I want to be the one who decides when it runs. Background knowledge that only makes Claude better-informed gets user-invocable: false and stays out of my way.
Built-in skills
The set moves between releases and depends on which plugins you have enabled, so treat any list in a blog post as a snapshot — /help shows what’s actually loaded in your setup. The ones I reach for:
| Skill | What it does |
|---|---|
/code-review |
Reviews the current diff, a branch, or a PR |
/simplify [focus] |
Reviews changed code for reuse and simplification, then applies the fixes |
/loop [interval] <prompt> |
Runs a prompt on a schedule |
/run |
Launches the project’s app to check a change in the real thing |
/claude-api |
Loads the Claude API reference |
What a real one looks like
Everything above is a template. This is the Mastodon skill from the opener, with the parts specific to my server taken out — 156 lines, two frontmatter fields, no clever features at all.
---
name: mastodon-upgrade
description: Upgrade the self-hosted Mastodon instance (DigitalOcean droplet,
Docker Compose) to a newer release. Use when asked to update/upgrade Mastodon,
check whether the instance is behind upstream, or roll back a bad upgrade.
Also covers post-upgrade verification and disk cleanup of stale images.
---
# Mastodon upgrade
Read the target release's notes **before** touching the server — patch releases
are usually a tag bump, but minor and major ones carry migrations and `.env`
changes that this procedure does not cover blindly.
## The environment
[table: which host, which compose project, which services, where backups go,
what else runs on the box and must not be touched]
## Before you start
1. Read the release notes for every version between current and target
2. Check free disk — a pull needs ~1.2 GB
3. If the jump crosses a minor version, stop and read properly
## Procedure
### 1. Back up the database
### 2. Bump the tags
### 3. Pull and restart
### 4. Verify
### 5. Clean up old images
Two things make it work, and neither one is a feature from the table above.
The description lists the phrases I actually type — “update Mastodon”, “is it behind upstream”, “roll back” — instead of describing the capability in the abstract. That’s the difference between a skill that loads itself and one that sits there.
And the body is specific to the point of being useless to anyone else: which three tags, in what order, what a healthy backup looks like, which services on that box are somebody else’s. That specificity is the entire value. A skill that says “deploy carefully” is worth nothing — Claude already knows to be careful, it just doesn’t know that the backup script lives where it lives.
Starting points
These are templates rather than skills I run — the shape is right, the details are yours to fill in.
Commit helper
---
name: commit
description: Create a well-formatted git commit
disable-model-invocation: true
allowed-tools: Bash(git *)
---
Create a git commit:
1. Run `git diff --staged` to see changes
2. Write commit message in format: `type(scope): description`
Types: feat, fix, docs, style, refactor, test, chore
3. Keep subject under 72 characters
4. Add body if changes are complex
$ARGUMENTS
Code review
---
name: review
description: Review code changes for quality, security, and best practices
context: fork
agent: Explore
allowed-tools: Read, Grep, Glob, Bash(git *)
---
Review the recent changes:
## Changes to review
!`git diff HEAD~1`
## Review checklist
- [ ] Code quality and readability
- [ ] Security issues (injections, exposed secrets)
- [ ] Error handling
- [ ] Test coverage
- [ ] Performance implications
Provide feedback organized by: Critical -> Warnings -> Suggestions
Session logger
---
name: session-log
description: Log this session's activity
disable-model-invocation: true
---
Append a summary of this session to logs/${CLAUDE_SESSION_ID}.log:
- What was accomplished
- Files changed
- Decisions made
- Next steps
$ARGUMENTS
Troubleshooting
When a skill feels flaky, it’s usually one of three things: vague description, wrong invocation mode, or too much crammed into one file.
The first is the one that gets me. I once spent fifteen minutes on a skill that wouldn’t auto-trigger — checked the path, the name, the frontmatter syntax, all fine. The description said “creates a summary” and I kept asking Claude to “write a session log”. Zero overlap between those two phrases, so it never loaded. That’s the lesson behind writing descriptions in the words you actually type: you are not writing documentation, you are writing the search query your future self will accidentally use.
Skill doesn’t trigger automatically:
- Check that
descriptioncontains keywords you use - Ask Claude “what skills are available?” — verify the skill is listed
- Invoke manually with
/skill-nameto make sure it works
Skill triggers too often:
- Make
descriptionmore specific - Add
disable-model-invocation: true
Description gets truncated:
- Front-load the essentials — the first 250 characters matter most