Your First Skill
A skill is a folder with one Markdown file that teaches Claude a repeatable job. This chapter builds three of them on the bookshelf project, step by step, and runs each one for real: one that explains an endpoint in a read-only subagent, one that turns a commit range into release notes using live git output, and one that Claude picks up on its own. It ends with a troubleshooting section that includes a result I did not expect.
What a Skill Is, and What It Is Not
A skill is a directory containing SKILL.md: optional YAML frontmatter, then instructions in Markdown. Claude always sees the short description of every skill; the full body loads only when the skill is invoked, by you typing /name or by Claude deciding it fits (Source 1). That makes skills cheap to have many of.
| Use… | when you want… |
|---|---|
| CLAUDE.md | Facts Claude should know in every session (build command, conventions) |
| A skill | A procedure you repeat, loaded only when needed |
| A subagent | A separate context with its own tools (a skill can run in one with context: fork) |
| A hook | A rule that must run every time, enforced by the harness, not by Claude's judgement |
Older custom commands in .claude/commands/ still work; skills are the current form and add supporting files and the frontmatter below (Source 1).
Anatomy, Locations and Frontmatter
Where a skill lives decides who gets it: enterprise (managed settings), personal, project, nested subdirectories, extra directories, and plugins. If names clash, enterprise beats personal and personal beats project (Source 1). The frontmatter fields, all optional:
| Field | What it does |
|---|---|
name | The /name. Defaults to the directory name |
description | What Claude reads to decide whether to load it. If omitted, the first body line is used. Combined with when_to_use it is cut at 1,536 characters, so put the main use first |
argument-hint | Autocomplete hint such as [commit-range] |
arguments | Named positional arguments for $name substitution |
disable-model-invocation | true stops Claude loading it automatically; only /name runs it |
user-invocable | false hides it from the / menu; only Claude can use it |
allowed-tools | Pre-approves tools for the turn the skill runs in. It does not restrict other tools |
disallowed-tools | Removes tools while the skill is active |
model, effort | Override for the current turn only |
context: fork and agent | Run in a subagent; agent picks its type (default general-purpose) |
paths | Globs: auto-load only when matching files are in play |
hooks, shell | Hooks registered while the skill is used; shell for injected commands |
In the body, $ARGUMENTS is everything typed after the name, and $0, $1 are positional. A line starting with ! followed by a backticked command runs that command first and replaces the line with its output before Claude sees the skill. Edits to existing skill files are picked up live; a brand-new top-level skills directory needs /reload-skills (Source 1).
name, description, license, compatibility, metadata, allowed-tools) and reject others. A skill written for Claude Code may not upload unchanged.explain-endpoint: a Read-Only Explainer
The job: "How does this endpoint work, from controller to database?" You ask it often, and you want it to never edit anything. Two decisions shape the file: context: fork with agent: Explore runs it in a separate read-only subagent so its file reading does not clutter your conversation, and the body says what to report.
Steps: create .claude/skills/explain-endpoint/, write SKILL.md, then type /explain-endpoint GET /api/books/{id}.
The run below is the real thing. The command was sent through claude -p with the project's skills loaded and tools limited to Read, Grep, Glob and Skill.
Real sample: what Claude answered (Checked)
release-notes: Arguments and Live Command Output
The job: turn git log for a range into release notes without inventing anything. Three things are new: $ARGUMENTS carries the commit range, the ! line injects real log output before Claude reads the file, and disable-model-invocation: true means it runs only when you ask, because writing notes is a deliberate act.
allowed-tools: Bash(git log *) matters. Injected commands are checked against permission rules, and one that is not allowed aborts the whole invocation (Source 1). The pre-approval is narrow on purpose: git log only, for this turn only.
Real sample: what Claude answered (Checked)
Will Claude Pick It Up by Itself?
With disable-model-invocation left off, Claude may load a skill whenever the description fits the request. So the description is the trigger. State what the skill does first, then "Use when…" with the situations. To test it, ask in plain words without naming the skill, and look at the tool calls. This is the real log of that test, using explain-endpoint's description from above:
| # | Event | Content (shortened) |
|---|---|---|
| 1 | Skill call | {"skill": "explain-endpoint", "args": "title search endpoint"} |
| 2 | Answer shown to you | Base directory for this skill: <project>/.claude/skills/explain-endpoint Explain how `title search endpoint` is handled in this project. 1. Find the controller method that maps it. 2. Follow the call into the service and repository layers. 3. Name every clas… |
| 3 | Grep call | {"pattern": "title|search", "path": "<project>", "-i": true, "output_mode": "content", "glob": "**/*.java"} |
| 4 | Bash call | {"command": "cd <project>/src; cat -n main/java/com/example/bookshelf/BookController.java main/java/com/example/bookshelf/BookService.java main/java/com/example/bookshelf/BookRepository.java main/java/com/example/bookshelf/BookDto |
| 5 | Read call | {"file_path": "<project>/src/main/java/com/example/bookshelf/BookController.java"} |
| 6 | Read call | {"file_path": "<project>/src/main/java/com/example/bookshelf/BookService.java"} |
| 7 | Read call | {"file_path": "<project>/src/main/java/com/example/bookshelf/BookRepository.java"} |
| 8 | Read call | {"file_path": "<project>/src/test/java/com/example/bookshelf/BookSearchTest.java"} |
| 9 | Grep call | {"pattern": "statement|StatementCount|generate_statistics|getPrepareStatementCount", "path": "<project>", "-i": true, "output_mode": "content"} |
| 10 | Answer shown to you | `GET /api/books/search?title=...` does a case-insensitive "contains" match on title and returns the matches sorted by title. I didn't edit anything. **Call path** 1. **Controller:** `BookController.search(@RequestParam String title)` is mapped at lines 30-33 … |
From the same log: 2 turns, 38.0 s, estimated $0.146, model claude-sonnet-5-5.
Skill tool with explain-endpoint; the rest is the forked subagent doing the work. The review was good (see below), but one run is one run: with a vaguer description or a differently worded question it may not trigger. The negative case (a question that should not trigger it) was not run for this chapter.Author's check of this run's answer
Troubleshooting, With a Real Experiment
The most useful first check is whether Claude Code sees the skill at all. A session's start-up event lists the skills it loaded. I created two deliberately broken skills and started a one-turn session:
What I expected versus what happened. From the documentation I expected the blank-line skill to lose its frontmatter and the typo skill to load with the unknown key ignored. In this run neither appeared in the list at all. I did not establish why; one possibility is that a skill without a usable description is not listed, but that is a guess. What the experiment shows is the diagnostic: if a skill is not in the list, fix the file before blaming the description.
| Symptom | Check |
|---|---|
Not in the /skills list | Is --- the very first line? Is the YAML valid? Is the key spelled exactly? Is the name free of odd characters? claude --debug and search the log for [names] (Source 1) |
| Listed but Claude never uses it | Is disable-model-invocation set? Rewrite the description: main use first, concrete situations |
| You renamed a skill and nothing changed | Renames in unwatched locations need a restart; a new top-level skills folder needs /reload-skills |
| An injected command aborts the skill | Add the narrowest allowed-tools pattern; a failing command aborts the invocation, so append || true to commands that may exit non-zero |
| Claude stops following it mid-session | The page has a section on this that I did not read; re-invoke the skill, and keep the body short and imperative |
allowed-tools.Key Takeaways, Exercises and Sources
- A skill is a folder with SKILL.md. The description is always loaded; the body loads when used.
- The description is the trigger. Main use first, then "Use when…".
- Use
context: forkwith a read-only agent for explorers that should not touch files or clutter your chat. - Inject facts with
!lines, and give them the narrowestallowed-tools. - Tell the skill what not to do. The best line in release-notes is the "Needs a human" rule.
- If it is not listed, fix the file first.
Exercise 1 Beginner
Write a skill called explain-class that explains one class in a repository you know. Use context: fork and a read-only agent. Run it with /explain-class Name.
Learning outcome: create, place and invoke a skill.
Exercise 2 Intermediate
Make a skill that injects git status --short with a ! line. First leave out allowed-tools and see what happens, then add the narrowest pattern that works.
Learning outcome: understand permissions on injected commands.
Exercise 3 Advanced
Write three prompts that should trigger one of your skills and three that should not. Run all six with --output-format stream-json --verbose and count the Skill tool calls. Rewrite the description until all six behave.
Learning outcome: test a skill's trigger like code.
- Anthropic, Extend Claude with skills — locations, frontmatter, arguments, dynamic injection, supporting files, listing limits, troubleshooting. The last roughly 19,000 characters of the page were not read.
- Anthropic, Subagents — the Explore agent used with
context: fork.
The runs SK-01, SK-02, SK-04 and SK-05 were executed on 2026-10-31 against the bookshelf project (tools/capture.sh and tools/stream_once.sh); project skills were loaded with --setting-sources user,project, used only on a repository I wrote. The skill files are in the examples repository under skills/. The cause of the unlisted broken skills was not established.