A Claude Skill is a reusable workflow — written in markdown, stored in a .claude/skills/ directory, and surfaced to Claude through its description. The point of this tutorial is to walk through every moving part of a skill, from frontmatter to scripts to allowed-tools, then ship a working example you can paste into your own repo today.
Skills are one of the three primitives Claude Code gives agentic teams — alongside subagents and hooks — and they're arguably the most under-used. A skill is just an instruction document plus a workflow body plus permission rails. When the description is right, Claude discovers the skill, invokes it, executes it, and reports back. No glue code, no MCP server, no separate process.
This guide covers the anatomy of a skill, the frontmatter fields that determine discoverability and blast radius, the description-design rules that decide whether Claude actually auto-invokes it, and a paste-ready blog-stats implementation you can adapt to your stack. Everything below matches the current Anthropic Claude Code documentation as of May 2026.
- 01Skills are named workflows plus context — markdown is the spec.A SKILL.md file with frontmatter and a workflow body becomes a discoverable skill. No separate metadata file. The markdown is the entire contract.
- 02Description is what makes it discoverable to Claude (and to humans).Trigger-phrase laden, action-oriented, specific. Vague descriptions mean Claude won't auto-invoke. The description field is read at every prompt; treat it as a routing decision, not flavor text.
- 03allowed-tools narrows blast radius without limiting expressiveness.Glob-allowlists like Bash(npm *) prevent the skill from doing anything unrelated to its purpose. Most production skills only need three or four tool patterns to do their job.
- 04Project skills track in git; user skills don't.Team-shared workflows belong in the repo at .claude/skills/. Personal aliases belong in ~/.claude/skills/. On naming conflicts, personal scope overrides project scope (and enterprise managed settings override both) — design accordingly.
- 05Keep mutating skills explicit through description and permissions.For destructive or expensive workflows, make the description require explicit user intent and keep allowed-tools narrow. Let safe lookup skills stay easy to discover.
01 — What's a SkillReusable workflows, loaded on demand.
A skill is a single markdown file — SKILL.md — with a small amount of YAML frontmatter and a workflow body written for Claude to read. The frontmatter names the skill, describes it, gates the tools it can touch, and optionally disables auto invocation. The body is the instructions: what to do, in what order, with what guardrails. That's it. There is no compile step, no separate registration file, no JSON manifest. The markdown is the contract.
Skills sit in one of two directories. Project skills live at .claude/skills/<skill-name>/SKILL.md in the repo — they ship with the codebase and become part of the team's shared muscle memory. User skills live at ~/.claude/skills/<skill-name>/SKILL.md on the developer's machine — personal aliases that follow you across projects but never get committed.
Claude Code scans both directories on session start and registers each skill by name. A skill at .claude/skills/blog-stats/SKILL.md is available as blog-stats. The description field is indexed so Claude can auto-invoke the skill when a user's prompt matches the description's intent — that's the difference between a skill that gets used and one that sits idle, and the reason description design matters so much.
Skills vs subagents vs hooks
The three Claude Code primitives are easy to confuse. A quick disambiguation:
- Skills are reusable workflows. Claude loads them on demand, follows the instructions, executes the tools the frontmatter allows, and reports back in the same conversation. Best for: repeatable processes, lookup tasks, structured outputs.
- Subagents are forked Claude sessions with their own system prompt and tool allowlist. They run in their own context window and return a summary. Best for: parallel research, isolated investigations, tasks where you want to firewall context from the main session.
- Hooks are deterministic shell commands triggered by lifecycle events (PreToolUse, PostToolUse, Stop, SubagentStop). They run regardless of model decisions. Best for: formatters, validators, notifications, audit logging.
If you're tempted to write a subagent for something that's essentially "read a config and run a script," it's probably a skill. If you're tempted to write a skill for something that needs to fire on every tool call, it's a hook. Skills are the right answer when the workflow is genuinely user-triggered and benefits from a human-readable name like blog-stats or release-notes.
02 — AnatomySKILL.md, scripts, references, colocated.
A skill is a directory, not a file. The directory name is the stable skill identifier: .claude/skills/blog-stats/ registers as blog-stats. Inside the directory, SKILL.md is the only required file. Everything else — scripts, reference docs, fixtures, templates — is optional but colocated so the entire skill is a self-contained unit that can be reviewed, version-pinned, and forked.
Colocation matters more than it looks. When a teammate opens review on a PR touching .claude/skills/release-notes/, every piece of the skill is right there: the workflow body, the shell script it calls, the markdown reference it cites, the example output it targets. No hunting through scripts/ or docs/ to reconstruct what the skill does.
The contract
Frontmatter (name, description, allowed-tools) plus a workflow body. Claude reads this top-to-bottom every time the skill fires.
Callable scripts
Node, bash, python — anything the skill needs to execute. The body invokes them via Bash. Keep them small, idempotent, and documented inline so review is cheap.
Reference docs
Style guides, prompt templates, lookup tables, decision matrices. The workflow body cites them by relative path. Edits to reference docs reshape skill behavior without changing the contract.
Fixtures & templates
Schema snippets, response templates, configuration baselines. Useful when the skill produces structured output and you want the format pinned to a file, not buried in the prose.
Most production skills have a SKILL.md plus one or two supporting files. Skills that grow beyond five files are usually doing too much — that's the signal to split into two skills, or to extract a subagent. Skills are deliberately lightweight; the value comes from having dozens of them, not from any one being elaborate.
03 — Frontmattername, description, allowed-tools.
Every SKILL.md opens with YAML frontmatter. Per the Claude Code skills docs, the routing surface is intentionally small: name, description, and allowed-tools. A minimal frontmatter block looks like this:
---
name: blog-stats
description: Generate a stats digest for the Digital Applied blog — post counts by category, average reading time, recent publishing cadence. Use when the user asks for "blog stats", "blog metrics", "how many posts in <category>", or wants a snapshot of editorial output.
allowed-tools:
- Bash(node *)
- Bash(git log *)
- Read(lib/blog-posts.ts)
---name
The kebab-case display name. Lowercase letters, numbers, and hyphens only. Optional — if you omit it, Claude Code uses the directory name. Convention is two-to-four words separated by hyphens. Pick a name a teammate could guess from the workflow's intent.
description
The most important field. Claude reads this on every user prompt to decide whether to auto-invoke the skill. Write it as a routing instruction: what the skill does, plus the trigger phrases that should fire it. The next section covers the rules in depth. Keep it concise — the documented description cap is 1,536 characters, so put the key use case first.
allowed-tools
A list of tool-pattern strings the skill is permitted to call. If omitted, the skill inherits the session's allowlist (everything). Specifying allowed-tools narrows the skill's blast radius — see the dedicated section below. Patterns use glob syntax: Bash(npm *), Read(lib/*), mcp__supabase__*.
Generate a report
Multi-step process that produces a structured artifact. Calls scripts, reads files, formats output. The most common skill archetype — anything that turns inputs into a digest, a changelog, or a brief.
Answer a factual question
Read a source of truth (API, database, doc), return a focused answer. No mutations. Fast, cheap, safe to auto-invoke — this is where the description-design rules earn their keep.
Mutate state
Writes to a database, hits a deploy endpoint, modifies production state. Make the description require explicit user intent, narrow the tool allowlist hard, and keep human approval in the workflow.
The frontmatter is the routing layer. Workflow and lookup skills should be easy to discover; action skills should require explicit user intent. A team library of ten lookup skills plus three action skills plus two workflow skills is a healthy distribution — most calls go through safe lookup skills, the destructive ones stay tightly permissioned.
04 — Description DesignKeyword-rich, trigger-phrase-laden, specific.
The description field is the difference between a skill that gets auto-invoked and one that sits dormant until somebody asks for it explicitly. Claude reads every skill's description at every prompt and decides — fast — whether the prompt matches. Vague descriptions don't match. Specific descriptions match well. Descriptions stuffed with the exact trigger phrases users say match best.
The four rules
- Lead with the action. "Generate a stats digest…" not "A skill for generating stats…". Verb-first matches the way users phrase requests.
- Include trigger phrases verbatim. If users say "blog stats", "how many posts", "post breakdown" — write those phrases into the description. Claude matches against the words, not the gist.
- Specify the scope. "Digital Applied blog" (specific) beats "the blog" (vague). The more concrete the scope, the less likely Claude misfires on adjacent requests.
- Stay under the 1,536-character cap. Per the skills docs, the
descriptionfield is capped at 1,536 characters. Put the key use case first — past that ceiling, the routing model never sees the rest.
Good vs bad — same skill
Bad: "A skill for analyzing the blog. Useful for reporting." Eight words, no triggers, no scope, no action shape. Claude will never auto-invoke this — there's nothing to match against.
Good: "Generate a stats digest for the Digital Applied blog — post counts by category, average reading time, recent publishing cadence. Use when the user asks for 'blog stats', 'blog metrics', 'how many posts in <category>', or wants a snapshot of editorial output." Three explicit trigger phrases in quotes, named output shape, scoped to a specific blog. Claude reliably fires on "blog stats" or "how many posts in AI Development".
"Description is what helps Claude decide when to load the skill automatically — the key use case goes first, because the description field is capped at 1,536 characters."— Paraphrased from the Claude Code skills frontmatter reference
One field shapes everything downstream. A team can write the best workflow body in the world and the skill still won't get used if the description is vague. Conversely, a one-paragraph workflow with a tight description gets reached for daily. Spend more time on the description than on the body — the body is read once per invocation, the description is read at every prompt.
05 — Allowed ToolsGlob-allowlists that prevent surprises.
The allowed-tools frontmatter field gates which tools the skill is permitted to call. Each entry is a glob pattern. If the field is omitted, the skill inherits the session's full allowlist — usually too wide. Specifying allowed-tools is the single best way to make a skill safe to auto-invoke.
Pattern syntax
Tool patterns follow Claude Code's standard tool-name glob format. Common shapes:
Bash(git *)— any git subcommand. Allowsgit log,git status,git diffbut notrm, notcurl.Bash(npm *)— npm operations. Permits builds, installs, and audits but not arbitrary shell.Read(lib/*)— file reads underlib/. Useful when a skill consults a registry likelib/blog-posts.tsbut shouldn't poke intoapp/ornode_modules/.mcp__supabase__*— every Supabase MCP tool. Useful for skills that hit a database; pair with a description that requires explicit user intent if the skill mutates state.Bash(node scripts/blog-*.mjs)— a specific script family. The tightest pattern; appropriate when the skill is essentially a wrapper around one script.
The security model
Claude Code enforces tool patterns at the call site. If a skill tries to call a tool outside its allowlist, the call fails with a permission error — the skill can recover and report the failure, but it can't bypass the gate. This means the allowed-tools field is the trust boundary; everything inside the skill body operates under that ceiling.
The corollary: don't grant tools you don't need. A lookup skill that only reads files should not list Bash(*). A skill that runs one script should not list Bash(npm *). Narrow allowlists let you keep discoverability on while keeping the blast radius small.
Report generator
Reads a registry, runs an analyzer script, formats markdown. Tight allowlist: Read(lib/*) plus Bash(node scripts/<family>-*.mjs). No git, no network, no arbitrary shell. Safe to auto-invoke.
CRM query
Hits an MCP server, returns a count or a breakdown. Allowlist is the MCP namespace plus zero local tools — mcp__zoho__* and nothing else. Description includes the trigger phrases users actually say.
Database mutation
Writes to production. Make the description explicit about mutation, narrow Bash to the one deploy script, and require human approval before the write. Blast radius minimized.
Toolchain updater
Runs brew, npm, pnpm, mas. Allowlist Bash(brew *), Bash(npm *), Bash(pnpm *) — but never Bash(*). The script can still call dozens of commands; the gate keeps it inside the toolchain family.
The allowlist is also documentation. A reviewer skimming the frontmatter sees exactly what the skill is permitted to touch — no need to read the body to find out whether the skill might shell out to curl. Treat allowed-tools as part of the contract, not an afterthought.
06 — Worked ExampleBuilding blog-stats — paste-ready.
Time to ship a real skill. blog-stats is a project skill that reads the blog registry, calls a Node analyzer, and returns a markdown digest — post counts by category, average reading time, the last five published slugs, and a one-paragraph summary. The full file pair below drops into .claude/skills/blog-stats/ with no other changes.
Step 1 — create the directory
From the repo root, run:
mkdir -p .claude/skills/blog-stats
touch .claude/skills/blog-stats/SKILL.md
touch .claude/skills/blog-stats/analyze.mjsStep 2 — write SKILL.md
The full file. Frontmatter at the top, workflow body below, references to the script and the registry. Paste this verbatim:
---
name: blog-stats
description: Generate a stats digest for the Digital Applied blog — post counts by category, average reading time, recent publishing cadence, and the last five published slugs. Use when the user asks for "blog stats", "blog metrics", "how many posts in <category>", "publishing cadence", or wants a snapshot of editorial output. Reads lib/blog-posts.ts as the source of truth.
allowed-tools:
- Read(lib/blog-posts.ts)
- Bash(node .claude/skills/blog-stats/analyze.mjs *)
---
# blog-stats
Generate a stats digest for the Digital Applied blog.
## When to fire
Auto-invoke when the user asks for any of:
- "blog stats" or "blog metrics"
- "how many posts in <category>"
- "publishing cadence" or "recent posts"
- "editorial output" snapshot
## Workflow
1. Read `lib/blog-posts.ts` to confirm the registry is present.
2. Run the analyzer:
```
node .claude/skills/blog-stats/analyze.mjs
```
Pass `--category="<name>"` if the user named a specific category.
3. Format the analyzer output as a markdown digest with these sections:
- **Total posts** (one line)
- **By category** (table — category, count, % share)
- **Reading time** (mean, median, range)
- **Recent five** (slugs with publish dates)
- **One-paragraph summary** — call out the highest-volume category and the cadence trend over the last 30 days.
4. Return the digest in the conversation. Do not write a file unless the user asks.
## Constraints
- Read-only. Never mutate the registry from this skill.
- If the registry shape changes (new field, renamed field), report the discrepancy and stop — don't guess.
- Cap the digest at 400 words; if categories explode, group the long tail under "Other".Step 3 — write analyze.mjs
The supporting script. Parses the TypeScript registry as text (no transpilation), aggregates the fields the workflow needs, prints JSON to stdout. Paste verbatim:
#!/usr/bin/env node
// .claude/skills/blog-stats/analyze.mjs
// Reads lib/blog-posts.ts and emits aggregate stats as JSON.
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const REGISTRY = resolve(process.cwd(), "lib/blog-posts.ts");
const ARG_CATEGORY = (process.argv.find((a) => a.startsWith("--category=")) ?? "").split("=")[1];
const src = readFileSync(REGISTRY, "utf8");
// Coarse parser — registry entries are well-formed object literals, so a
// per-block regex is sufficient. Don't try to TypeScript-parse this; the
// shape is stable enough to grep.
const entryRe = /\{\s*slug:\s*"([^"]+)",\s*title:\s*"([^"]+)",[\s\S]*?category:\s*"([^"]+)",[\s\S]*?readingTime:\s*(\d+),[\s\S]*?publishedTime:\s*"([^"]+)"/g;
const posts = [];
let m;
while ((m = entryRe.exec(src)) !== null) {
const [, slug, title, category, readingTime, publishedTime] = m;
if (ARG_CATEGORY && category !== ARG_CATEGORY) continue;
posts.push({ slug, title, category, readingTime: Number(readingTime), publishedTime });
}
const byCategory = posts.reduce((acc, p) => {
acc[p.category] = (acc[p.category] ?? 0) + 1;
return acc;
}, {});
const readingTimes = posts.map((p) => p.readingTime).sort((a, b) => a - b);
const mean = readingTimes.reduce((a, b) => a + b, 0) / (readingTimes.length || 1);
const median = readingTimes[Math.floor(readingTimes.length / 2)] ?? 0;
const recent = posts
.slice()
.sort((a, b) => b.publishedTime.localeCompare(a.publishedTime))
.slice(0, 5)
.map((p) => ({ slug: p.slug, publishedTime: p.publishedTime }));
const out = {
total: posts.length,
byCategory,
readingTime: {
mean: Number(mean.toFixed(1)),
median,
min: readingTimes[0] ?? 0,
max: readingTimes[readingTimes.length - 1] ?? 0,
},
recent,
filter: ARG_CATEGORY ?? null,
};
console.log(JSON.stringify(out, null, 2));Step 4 — try it
Save both files. Restart your Claude Code session — the skill registry is scanned at session start. Ask something like "give me blog stats" or "how many posts in AI Development?" and Claude can route to the skill from the description. Claude reads the SKILL.md, runs the analyzer, formats the output, and returns the digest. The entire flow adds only the analyzer runtime plus the model turn.
.claude/skills/blog-stats/, no other edits. The skill is already discoverable to Claude on the next session start. Commit both, and your team gets the shared project skill at their next git pull. That's the entire shipping ceremony for a project skill.Skim the SKILL.md once more. The whole contract is in plain markdown — anyone on the team can review it, fork it, tighten the allowlist, or rewrite the workflow body without learning a new tool. That's the leverage skills offer: a Git-friendly, review-friendly, paste-ready unit of agentic capability that grows with the codebase.
07 — Scope & ShareUser scope, project scope, plugin distribution.
Skills live in one of two directories — and the choice determines who gets them, whether they version with the codebase, and what happens when two skills have the same name. The split is simple, but getting it right matters more than it sounds.
Project scope — .claude/skills/
Lives in the repo. Tracks in git. Ships with the codebase. Every teammate who clones the project gets every project skill on their next session start — no setup ceremony, no install command. This is the right place for anything tied to the project's stack, conventions, registries, or workflows.
Use project scope for: report generators (blog-stats, release-notes), lookups (component-docs, zoho-leads-report), action skills (deploy, rotate-keys), maintenance skills, onboarding flows. Anything a teammate could reasonably need within five minutes of cloning the repo.
User scope — ~/.claude/skills/
Lives in your home directory. Doesn't track in git. Doesn't ship with any codebase. Available in every Claude Code session you start, regardless of project. This is where personal aliases and cross-project skills live — workflows that follow you, not the repo.
Use user scope for: personal aliases (morning-standup, inbox-zero), cross-project workflows (audit-repo, preflight), and skills that depend on credentials or tools only on your machine. If a teammate would be confused or broken by the skill, it's user scope.
What happens on a naming conflict
Personal scope wins over project scope — per the Claude Code skills docs, enterprise managed settings override personal, and personal overrides project. So if you have ~/.claude/skills/audit-repo/ and the repo also ships .claude/skills/audit-repo/, the personal version is the one Claude registers as audit-repo — the project version is shadowed in your session. Design accordingly: don't shadow a project skill from user scope by accident, and if a project ships a skill you care about, either accept the project version or rename your personal alias. Plugin skills sit outside this hierarchy — they use a plugin-name:skill-name namespace so they cannot collide.
Distributing skills as plugins
Beyond the two built-in scopes, Claude Code supports plugin distribution — a third path where a separate package bundles skills (and subagents, hooks, MCP servers) and makes them installable via npx. Useful when you want to share a skill set across multiple repos that aren't worth coupling, or when you're publishing a public skill library.
For most teams, project scope plus a small user-scope library is enough — plugin distribution is the right answer once a skill set outgrows a single repo or starts shipping to outside consumers. Start with project scope; promote to a plugin when the same skill ends up duplicated in three repos.
For agencies and engineering teams formalizing their agentic stack, curating a project-scope skill library is the highest-leverage move we see in client engagements. Five to twenty well-named skills, tight allowlists, descriptions tuned for the team's actual phrasing — that's institutional memory rendered into Claude Code. Our AI digital transformation engagements usually start with exactly this exercise — naming, scoping, and calibrating a team's first ten skills.
Skills turn institutional knowledge into invocable workflows.
A Claude Skill is the simplest possible unit of agentic capability: a markdown file with frontmatter, an optional script or two, and a kebab-case directory name that becomes the skill identifier. The cost of writing one is an hour; the cost of the team adopting it is zero. Once SKILL.md lands in the repo, every teammate gets it on the next session start.
What changes when a team has ten to twenty skills isn't any single invocation — it's the shape of the work. Onboarding a new hire stops being a two-week shadowing exercise; it becomes "here are the slash commands, fire them as you work." Shared muscle memory gets rendered into executable documentation. The skills library is the institutional memory of the team, version-controlled, reviewable, and improvable by anyone with a PR.
What to build next: combine skills with subagents for compound leverage. A subagent can fire a skill; a skill can dispatch a subagent. The interesting workflows live in that combination — a release-notes skill that spawns three subagents to investigate categories in parallel, then merges the outputs through a template. Start with a few simple skills, get the description design right, then graduate to compound patterns once the library matures.
