Build your own Claude Code
Skills & slash commands·5 min

Anatomy of a skill

0 / 1

You have built an agent that reads, writes, runs commands, asks permission, spawns subagents, talks to MCP servers, and fires hooks. Every one of those is code you wrote. A skill is the escape hatch from that: a way to teach the agent a new procedure without touching its source — you drop a file on disk and the agent picks it up.

A skill is a single file: SKILL.md. It has two halves — YAML frontmatter (metadata) and a Markdown body (instructions). The cleverness is in when each half is loaded. The frontmatter's description is cheap and always in context so the model knows the skill exists; the body is expensive and loads only when the skill is actually invoked. That split is the entire reason skills scale: you can have a hundred of them and pay almost nothing until you call one.

.claude/skills/commit/SKILL.md
---
name: commit
description: Stage and commit the current changes. Use when the user asks to commit.
allowed-tools: Bash(git add *) Bash(git commit *) Bash(git status *)
disable-model-invocation: true
---
Stage the relevant files, write a conventional-commit message
summarizing the diff, and commit. Do not push.

The frontmatter is the part the agent reads at startup. Each field tunes one dimension of the skill:

  • name — the display label in skill listings. (It does not set the command you type — that comes from where the file lives. More on that next step.)
  • description — what the skill does and when to use it. This is the one field that matters: it is loaded into context for every session so the model can decide whether to reach for the skill. Put the key use case first; Claude Code truncates the listing at 1,536 characters.
  • allowed-tools — tools the skill may use without prompting you for permission while it is active (a space- or comma-separated string, or a YAML list). It grants, it does not restrict — your permission settings still govern everything else.
  • disable-model-invocation — set true to make the skill user-only: you can still type /commit, but the model can never trigger it on its own. Use it for anything with side effects you want to time yourself — deploys, commits, sending messages.

Everything after the closing --- is the body — the instructions the model follows once the skill runs. This is where the procedure lives: the checklist, the steps, the gotchas. It can be long, because it costs nothing until invoked. The body can also reference dynamic values ($ARGUMENTS, session/skill-dir variables) that get filled in at invoke time — you will implement that expansion in step 03.

The load split is the whole design:

  • Description → loaded at startup. Cheap. Always present so the model knows the skill is available.
  • Body → loaded on invoke. Expensive. Pulled in only when the skill is actually called, by /name or by the model deciding the description matches.
Description-at-startup, body-on-invoke

This is the sentence to remember: the description is always in context; the body is not. In a normal session every skill's description is loaded so the model knows what is available, but the full content only loads when the skill is invoked. That is why a skill body can hold a thousand lines of reference material and still cost you nothing on a session where you never call it — the opposite of stuffing the same text into CLAUDE.md, which is paid for on every turn.

One exception worth noting: a skill with disable-model-invocation: true keeps its description out of context entirely (the model can't invoke it, so it doesn't need to know it exists) — the body still loads when you invoke it.

Skills vs. legacy slash commands

Before skills, custom slash commands were a single Markdown file: .claude/commands/deploy.md created /deploy. Custom commands have been merged into skills. Both of these create /deploy and run the same way:

  • Legacy command — .claude/commands/deploy.md (one file, the filename is the command name).
  • Skill — .claude/skills/deploy/SKILL.md (a directory named deploy, with SKILL.md inside; the directory name is the command name).

Existing .claude/commands/ files keep working and support the same frontmatter. The skill form adds three things the flat file can't: a directory to hold supporting files, frontmatter to control who invokes it (disable-model-invocation), and the ability for the model to load it automatically when the description matches. If a skill and a command share a name, the skill wins.

Where skills and commands live
.claude/
  skills/
    deploy/
      SKILL.md          → /deploy        (directory name is the command)
      helper.py         (supporting file, only the skill dir can hold these)
  commands/
    review.md           → /review        (legacy: filename is the command)

~/.claude/skills/...    user-level skills, available across all projects