Blog · AI Harness

How to create a Skill in Claude Code: SKILL.md step by step, with a script and arguments

Capiva · October 2026 · 9 min read

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 contentWhere 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/scripts

Where the folder lives decides who sees the skill:

LocationPathApplies to
Personal~/.claude/skills/<name>/SKILL.mdYou, in every project
Project.claude/skills/<name>/SKILL.mdEveryone in the repository (goes into git)
Plugin<plugin>/skills/<name>/SKILL.mdWhoever 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:

  • description is 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 and when_to_use together are cut at 1,536 characters in the listing.
  • argument-hint shows up in autocomplete. Whatever comes after /new-adr replaces $ARGUMENTS in 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 of allowed-tools.
  • allowed-tools grants 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: 0015

Validate 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 passed

With 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 failed

Put 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 Redis

Before 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 vague description, or disable-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 to allowed-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 hooks field). 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 /doctor to see the cost and disable the ones you do not use.
  • [ ] On Windows, ! does not run. The default is bash. With shell: powershell in 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

Want a full workflow of skills, hooks and gates ready for your team? See capivaOS.