# Write a Claude Code Skill That Loads Only When It's Needed: SKILL.md Step by Step

> Source: <https://dev.to/gabrielanhaia/write-a-claude-code-skill-that-loads-only-when-its-needed-skillmd-step-by-step-44n6>
> Published: 2026-10-15 19:45:16+00:00

Open your CLAUDE.md and scroll. Somewhere past the build commands there is a section called "Cutting a release": how to find the last tag, which commit types to drop, the heading order, the tone. Forty lines you need maybe twice a month. They sit in the context window for every turn of every session, including the afternoon you spend fixing a CSS bug.

That is how CLAUDE.md works by design. [Claude Code reads it at the start of every session](https://code.claude.com/docs/en/memory), and the same page tells you to target under 200 lines per file and to move "a multi-step procedure" into a skill instead. A skill's body stays on disk until Claude decides the task needs it. The only part in context up front is a name and a one-paragraph description.

Move that procedure into a [Claude Code](https://code.claude.com/docs/en/overview) skill: a `SKILL.md`, a bundled script, and a style reference that loads only when the notes get written.

Anthropic's [Agent Skills overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) splits a skill into three levels, each loaded at a different time:

| Level | When it loads | Cost per the docs | 
|---|---|---|
| `name` +`description` | Always, at startup | About 100 tokens per skill | 
| `SKILL.md` body | When the skill triggers | Under 5k tokens | 
| Bundled files and scripts | When read or run | Nothing until accessed | 

Scripts get an extra discount. When Claude runs a bundled script, [only its output enters context](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview), not the code. So the deterministic work (listing commits, grouping them) belongs in a script, and the judgment (which commits a user cares about) belongs in markdown.

One caveat before you move everything out of CLAUDE.md. Once a skill is invoked, its content [stays in the conversation](https://code.claude.com/docs/en/skills) for the rest of the session, and Claude Code does not re-read the file on later turns. So the saving is in the sessions that never touch releases. In the release session itself, the body sits in context from the moment the skill fires.

*Only the index card is always on the desk. The folder and the drawer open when the task needs them.*

Project skills live in `.claude/skills/<name>/SKILL.md`, personal ones in `~/.claude/skills/<name>/SKILL.md` ([skills docs](https://code.claude.com/docs/en/skills)). Use the project location so the skill ships with the repo:

```
.claude/skills/release-notes/
├── SKILL.md
├── reference/
│   └── style.md
└── scripts/
    └── commits.sh
```

Start with the script. It finds the last tag (or takes one as an argument), lists non-merge commits since then, and tags each line with its [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) type so the model does not have to parse prefixes:

``` bash
#!/usr/bin/env bash
# Usage: commits.sh [from-tag]
# Prints "<type>\t<hash> <subject>" for each commit
# since the tag, grouped by type.
set -euo pipefail

from="${1:-}"
if [ -z "$from" ]; then
  from="$(git describe --tags --abbrev=0 2>/dev/null \
    || true)"
fi

if [ -n "$from" ]; then
  echo "# commits since $from"
  range="$from..HEAD"
else
  echo "# no tag found, listing all commits"
  range="HEAD"
fi

git log "$range" --no-merges --pretty='format:%h %s' |
  awk '
    { type = "other" }
    $2 ~ /^feat(\(.*\))?!?:/ { type = "feat" }
    $2 ~ /^fix(\(.*\))?!?:/  { type = "fix" }
    { print type "\t" $0 }
  ' | sort -s -k1,1
```

[`git describe --tags --abbrev=0`](https://git-scm.com/docs/git-describe) prints only the closest tag reachable from `HEAD`, lightweight tags included. The `|| true` keeps `set -e` from killing the script in a repo with no tags yet.

Now the style reference. This is the forty lines that used to live in CLAUDE.md, cut down to what the model cannot guess:

```
# Release notes style

## Headings
Use these, in this order, and skip empty ones:
### Features, ### Fixes, ### Other

## Bullets
- Start with a past-tense verb: "Added", "Fixed".
- Describe the effect for the user, not the code.
- End with the short hash in parentheses: (a1b2c3d).
- A `!` right before the colon marks a breaking change.
  List it first under its heading, prefixed **Breaking:**.

## Leave out
- Refactors with no visible effect.
- Dependency bumps, unless they fix a security issue.
```

Then `SKILL.md`, which ties the two together:

```
---
name: release-notes
description: >-
  Drafts user-facing release notes from the git commits
  since the last tag, grouped into Features, Fixes and
  Other. Use when the user asks for release notes, a
  changelog entry, what shipped since a version, or a
  summary of changes for a release. Not for single
  commit messages or PR descriptions.
---

# Release notes

1. Get the commits by running:
   `bash "${CLAUDE_SKILL_DIR}/scripts/commits.sh" [tag]`
   Pass a tag only if the user named one.
2. Read [reference/style.md](reference/style.md)
   before writing anything.
3. One bullet per user-visible change. Merge commits
   that describe the same change. Drop chore, ci, test
   and docs commits unless the user asks for them.
4. Reply with Markdown only. Edit CHANGELOG.md only
   if the user asks you to.
```

The `>-` is plain YAML: it folds the indented lines into one string, so a long description stays readable and a stray colon inside it cannot break the parse.

`${CLAUDE_SKILL_DIR}` is a [substitution Claude Code fills in](https://code.claude.com/docs/en/skills) with the skill's own directory, so the script path works from any working directory. The `name` must be lowercase letters, numbers and hyphens, and on Anthropic's other surfaces it [cannot contain "anthropic" or "claude"](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices). The reference file is linked directly from `SKILL.md`, one level deep, because the best-practices guide warns that Claude may only partially read files reached through a chain of references.

You could also inline the script with the skills docs' inline shell syntax (an exclamation mark followed by a backticked command), which runs before Claude sees the skill and pastes the output in. The docs note that a failing command [aborts the whole invocation](https://code.claude.com/docs/en/skills), and the tag argument here is optional, so letting Claude run the script is the simpler choice.

Claude Code [watches skill directories](https://code.claude.com/docs/en/skills) and picks up new or edited skills mid-session. If `.claude/skills/` did not exist when the session started, Claude Code is not watching it yet: run `/reload-skills` to pick the skill up, and run it again after each later edit in that session, or start a new session.

Start a fresh session in a repo that has at least one tag. First ask `What skills are available?`, one of the checks in the [troubleshooting guide](https://code.claude.com/docs/en/skills). `release-notes` should be in the list. If it shows up but never triggers, look at the YAML: when frontmatter fails to parse, the skill still loads with empty metadata, so `/release-notes` works but Claude has no description to match. Run `claude --debug` to see the parse error. One easy way to cause it: an unquoted one-line description containing a colon followed by a space.

Then run prompts on both sides of the line:

| Prompt | Should the skill load? | 
|---|---|
| Write release notes for everything since v1.4.0 | Yes | 
| What shipped since the last tag? | Yes | 
| Draft a CHANGELOG entry for this release | Yes | 
| Write a commit message for my staged changes | No | 
| Explain what git describe does | No | 

The "No" rows are the useful ones. They share vocabulary with the skill (commits, git, tags) and should still be handled without it. If the commit-message prompt pulls the skill in, the description is too broad.

To see what the skill changes, get a baseline. Turn it off in `.claude/settings.local.json` with [`skillOverrides`](https://code.claude.com/docs/en/skills), run the same prompts in a new session, then remove the override:

```
{
  "skillOverrides": {
    "release-notes": "off"
  }
}
```

For cost, the Skills row in `/context` reports the size of the skill listing after the budget is applied, so it matches what the model receives, and `/skill-doctor` (Claude Code v2.1.252 or later) [reports each skill's context cost and invocation count](https://code.claude.com/docs/en/skills). Five prompts by hand catch the obvious misses. To test more than five, the docs' [`skill-creator` plugin](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/skill-creator) generates should-trigger and should-not-trigger prompts, measures the hit rate and proposes description edits. Install it with `/plugin install skill-creator@claude-plugins-official`.

*The description is the only thing checked at the door. The body never gets a vote on whether it loads.*

Everything in `SKILL.md` below the frontmatter is invisible until the skill is chosen. The choice is made from the `description` alone, which Anthropic's overview says Claude ["matches your request against"](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview). A great body behind a vague description never runs.

**Say what and when.** The best-practices guide asks for both: what the skill does, and the triggers for using it. "Helps with releases" fails both halves. "Drafts release notes from commits since the last tag. Use when the user asks for release notes, a changelog entry..." gives the model a task and a set of phrases to match.

**Write in third person.** The description is [injected into the system prompt](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices), and the guide warns that "I can help you" or "You can use this" can cause discovery problems. "Drafts release notes" reads correctly from any point of view.

**Use the words your users type.** Nobody asks for "a release artifact summary". They ask "what shipped since 1.4" or "write the changelog". Put those phrases in. The docs' first troubleshooting step for a skill that does not trigger is to check that the description "includes keywords users would naturally say".

**Put the key use case first.** In Claude Code the combined `description` and `when_to_use` text is [cut at 1,536 characters](https://code.claude.com/docs/en/skills) in the listing by default. Anything past that never reaches the model, so front-load the sentence that matters.

The whole listing also has a budget, 1% of the model's context window by default. If you have many skills and the listing overflows it, Claude Code drops whole descriptions, starting with the skills you invoke least. A new skill starts at zero invocations, so it is an early candidate. Check the Skills row in `/context`, and set low-priority skills to `"name-only"` in [`skillOverrides`](https://code.claude.com/docs/en/skills) to free room.

**Name the boundary.** The "Not for single commit messages or PR descriptions" line is a design choice of this tutorial rather than a rule from the docs. It earns its place when a nearby request keeps triggering the skill. The documented fix for over-triggering is to make the description more specific, and an explicit exclusion is one way to do that.

**Stay inside the portable limits.** On the Claude API and claude.ai, the description must be non-empty, [at most 1,024 characters](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) and free of XML tags. Claude Code's own docs only mention the 1,536-character listing cut, but staying under 1,024 keeps the skill portable to those surfaces.

If you only ever want release notes when you ask for them, add `disable-model-invocation: true` to the frontmatter. The [description then leaves context entirely](https://code.claude.com/docs/en/skills), and the skill runs only when you type `/release-notes`. That is the cheapest option, and the right one for anything with side effects you want to trigger by hand.

If a rule has to hold every time, such as "never push a tag without running the tests", a skill is the wrong home. The docs recommend a [hook](https://code.claude.com/docs/en/hooks-guide) for that, because a hook runs on its event whether or not Claude is following any skill. CLAUDE.md stays the place for facts every session needs: build commands, layout, conventions. Everything else can wait on disk until a request asks for it.

A skill description is a prompt with one job, getting picked for the right request, and that is the kind of prompt the [*Prompt Engineering Pocket Guide*](https://www.amazon.com/dp/B0GX38N645) spends its time on: being specific, choosing the words the model will match, and testing the misses as well as the hits. If you are trimming a CLAUDE.md that has grown into a manual, the same habits apply.
