A Claude Code skill is a folder with a SKILL.md file: YAML frontmatter that says when to use it and a Markdown body with the instructions. At startup, Claude Code loads only the name and description of each skill. The body enters the context when the skill is invoked, by you with /name or by Claude itself when the request matches the description. Skills follow the open Agent Skills standard, and the old commands in .claude/commands/ became skills: both formats create the same /name.
This post is one of the hands-on guides in the Claude Code guide. We will build a real skill, /new-adr, that records an architecture decision in the project's format, with a script that finds the next free number.
Decide whether it is a skill, CLAUDE.md or a hook
The skills docs give the criterion: create a skill when you keep pasting the same instructions, checklist or procedure into the chat, or when a section of CLAUDE.md turned into a procedure instead of a fact.
| Type of content | Where it goes |
|---|---|
| A fact that holds in every session ("port 5433, not 5432") | CLAUDE.md |
| A procedure used now and then ("how to record an ADR") | Skill |
| A rule that must always hold ("never edit a versioned migration") | Hook |
| A connection to an external system (database, API, ADRs through search) | MCP server |
Skills and MCP complement each other. The features overview sums it up: MCP connects Claude to external services and a skill teaches how to use those services. The skill in this post uses the ADR MCP server when it is connected.
Create the skill folder
mkdir -p .claude/skills/new-adr/scriptsWhere the folder lives decides who sees the skill:
| Location | Path | Applies to |
|---|---|---|
| Personal | ~/.claude/skills/<name>/SKILL.md | You, in every project |
| Project | .claude/skills/<name>/SKILL.md | Everyone in the repository (goes into git) |
| Plugin | <plugin>/skills/<name>/SKILL.md | Whoever installs the plugin, as /plugin:name |
The folder name becomes the command. So does the name field in the frontmatter, if there is one.
Write the SKILL.md
---
name: new-adr
description: Records an architecture decision as a new ADR in docs/adr, using the project template. Use when asked to record, document or formalize a technical decision.
argument-hint: "[decision title]"
allowed-tools: Read Write Bash(python3 ${CLAUDE_SKILL_DIR}/scripts/next_adr.py *)
---
## Existing ADRs and next number
!`python3 ${CLAUDE_SKILL_DIR}/scripts/next_adr.py ${CLAUDE_PROJECT_DIR}/docs/adr`
## How to record it
Requested title: $ARGUMENTS (if empty, ask for the title before continuing).
1. Look for related ADRs before writing. If the `adrs` MCP server is connected, use the `search_adrs` tool; if not, use Grep on `docs/adr/`. Cite the related ones in the Context section.
2. Create the file with the next number shown above, in the format `NNNN-title-in-kebab-case.md`, following the template in `${CLAUDE_SKILL_DIR}/template.md`.
3. Fill it in only with what was discussed in this conversation. Whatever was not discussed stays as "to be filled in". Do not invent alternatives.
4. Status is always `Proposed`. A person accepts it.
5. Finish by showing the path of the file you created.Line by line:
descriptionis the trigger. It is what Claude reads to decide whether to load the skill on its own. Put the main use case first, plus the words someone would use when asking for it ("record", "document", "technical decision"). The description andwhen_to_usetogether are cut at 1,536 characters in the listing.argument-hintshows up in autocomplete. Whatever comes after/new-adrreplaces$ARGUMENTSin the body.!followed by a command in backticks is context injection. Claude Code runs the command before sending the skill to the model and replaces the line with the output. Claude receives the ready list of ADRs, not an instruction to go get it.${CLAUDE_SKILL_DIR}is the skill folder, and${CLAUDE_PROJECT_DIR}is the project root. Both are substituted in the body and in the Bash rules ofallowed-tools.allowed-toolsgrants tools without asking permission, only in the turn where the skill is invoked. The Bash rule repeats the exact command from the body, with the same variable. That is the pattern the docs show for running an embedded script without a prompt.
That last point is not optional. Injected commands never ask for permission while the skill is being assembled. Outside auto mode, if the command's permission check does not return "allow", the invocation aborts with Shell command permission check failed. Without the rule in allowed-tools, the skill breaks on its first line.
The body follows the docs' advice: short, and written as a standing instruction. Once the skill loads, its content stays in the conversation for the following turns, and Claude Code does not re-read the file. Every line is a recurring context cost.
Add the script and the template
#!/usr/bin/env python3
"""List the existing ADRs and say which number is next."""
import re
import sys
from pathlib import Path
folder = Path(sys.argv[1] if len(sys.argv) > 1 else "docs/adr")
if not folder.is_dir():
print(
f"Folder {folder} does not exist. Create it first, or confirm the path with the user."
)
sys.exit(0) # exiting with an error would abort the skill; the message guides Claude
adrs = sorted(p for p in folder.glob("*.md") if re.match(r"\d{4}-", p.name))
for p in adrs:
title = p.read_text(encoding="utf-8").splitlines()[0].lstrip("# ").strip()
print(f"- {p.name}: {title}")
next_number = int(adrs[-1].name[:4]) + 1 if adrs else 1
print(f"\nNext number: {next_number:04d}")Notice the sys.exit(0) when the folder does not exist. Instead of breaking, the script prints a sentence that becomes an instruction for Claude. Supporting files like this one do not enter the context. The script runs and only its output appears.
The template sits next to it, in .claude/skills/new-adr/template.md, and is only read when the skill says so:
# ADR-NNNN: Title
## Status
Proposed
## Context
What problem forces the decision. Related ADRs.
### Options Considered
- Option A: pros and cons
- Option B: pros and cons
## Decision
What was decided, in one sentence, and why.
## Consequences
What gets easier, what gets harder, what needs to be revisited and when.Running the script on the example folder, with four real ADRs from capivaOS, this is what Claude receives in place of the ! line:
- 0008-phase-guard-hook-enforcement.md: ADR-0008: Hook-Enforced Phase Guards Parsing sprint-state.md Directly
- 0010-fast-lane-pipeline.md: ADR-0010: Fast-Lane Pipeline for Small, Low-Risk Tasks
- 0011-slim-always-loaded-layer.md: ADR-0011: Slim the Always-Loaded Instruction Layer
- 0014-autonomy-contract.md: ADR-0014: The Autonomy Contract — Delegated Gates, Isolation-First Context
Next number: 0015Validate the frontmatter
Broken YAML does not stop the skill from loading. It loads with empty metadata. /new-adr keeps working, but Claude will never trigger it on its own, because there is no description to compare against. To catch this early, starting with v2.1.233:
$ claude plugin validate .claude/skills
Validating components in: .../.claude/skills
✔ Validation passedWith a deliberately broken frontmatter, the same command fails and explains the consequence:
✘ Found 1 error:
❯ frontmatter: YAML frontmatter failed to parse: YAML Parse error: Unexpected token. At runtime this skill loads with empty metadata (all frontmatter fields silently dropped).
✘ Validation failedPut it in CI next to the CLAUDE.md linter. Another silent trap: a misspelled field name (allowed_tools with an underscore, for example) is ignored without an error.
Test both modes
Inside Claude Code, invoke it directly:
/new-adr Use Postgres as a queue instead of RedisBefore opening a session, you can confirm that Claude Code found the skill. Without login, claude -p "/new-adr Use Postgres" --debug-file debug.log ends with Not logged in · Please run /login, but the log already records the discovery (Claude Code 2.1.296):
[DEBUG] Loading skills from: managed=/etc/claude-code/.claude/skills, user=.../.claude/skills, project=[.../.claude/skills]
[DEBUG] Loaded 1 unique skills (1 unconditional, 0 conditional, managed: 0, user: 0, project: 1, additional: 0, legacy commands: 0)project: 1 is your skill. With project: 0, the path or the file name is wrong. The ! line does not run in this test; it only runs when the skill is actually invoked in a logged-in session.
Then test automatic triggering with a request that matches the description, without naming the skill: "record this decision to use Postgres as a queue". If Claude does not trigger it, the docs suggest three things: check that the description has the words people use, ask "What skills are available?" to see whether it shows up, and rephrase the request.
If the skill should only run when you ask (deploy, commit), add disable-model-invocation: true. If it is background knowledge that only Claude should use, user-invocable: false removes it from the / menu.
How we use it at Capiva
capivaOS is largely a set of skills. The workflow phases (grill-spec, plan, implement, test-verify, among others) are in the repository's skills folder. ADR-0011 records why this matters. The full examples for each phase used to live in always-loaded files, and they were moved so they only load in the phase that needs them. The always-loaded layer dropped from ~26.4k to ~16.9k tokens (estimated from characters/4), without cutting a single line of the examples.
Troubleshooting checklist
- [ ] The skill does not show up under
/. Check the path:.claude/skills/<name>/SKILL.md, with the file name in uppercase. The opening---has to be the first line of the file. - [ ] Claude never triggers it on its own. Broken YAML (run
claude plugin validate), a vaguedescription, ordisable-model-invocation: true. - [ ] It triggers too often. Make the description more specific or switch to manual invocation.
- [ ]
Shell command permission check failed. The command injected with!is not allowed. Add the exact rule toallowed-tools, or check whether a deny rule is catching it. - [ ] Claude stops following the skill mid-session. If it is a rule that must always hold, turn it into a hook (you can declare it in the frontmatter itself, in the
hooksfield). After compaction, only the start of each skill is kept. Invoke it again and put the essentials at the top. - [ ] Descriptions cut off with many skills installed. The listing has a budget of 1% of the context window. Run
/doctorto see the cost and disable the ones you do not use. - [ ] On Windows,
!does not run. The default is bash. Withshell: powershellin the frontmatter and the PowerShell tool enabled, injected commands run through PowerShell (see Claude Code on Windows).
Frequently asked questions
What is the difference between a skill and a command (.claude/commands)?▼
None when invoking: .claude/commands/deploy.md and .claude/skills/deploy/SKILL.md create the same /deploy. A skill adds a folder for supporting files and automatic triggering.
Does a skill work outside Claude Code?▼
The format follows the Agent Skills standard. But to upload it to claude.ai or the Skills API, only six frontmatter fields are accepted (name, description, license, compatibility, metadata, allowed-tools), and features like ! injection do not work there.
Skill or MCP?▼
MCP when you need to connect to a system; a skill when you need to teach a procedure. Often both together.
Can a project skill be dangerous?▼
Yes. The allowed-tools of a project skill applies even in a folder you never marked as trusted. Review a repository's skills before running Claude Code in it.
Read next
- Claude Code is a harness: CLAUDE.md, hooks, MCP and subagents (the main guide)
- How to write a CLAUDE.md the agent follows
- How to configure hooks in Claude Code
- How to build an MCP server in Python and configure it in Claude Code
- How to install Claude Code on Windows
Want a full workflow of skills, hooks and gates ready for your team? See capivaOS.