Claude Code Skills: How to Write Ones Your Agent Actually Uses
Why AI coding agents ignore skills, how to write descriptions that trigger in practice, and lessons from maintaining 130 skills across three active profiles.

You write a detailed skill, save it in your project, and ask your agent to handle a task. The model acknowledges your request, ignores the skill completely, and improvises its own implementation from scratch. With Claude Code skills, that often does not mean your instructions were wrong. It can mean the agent never read them.
That matters more as the collection grows. A skill is how you stop retyping the same procedure into every session, and I now have 130 of them across three Claude Code profiles. Until a skill is invoked, Claude sees only its frontmatter description, not the body. If the description is vague, opens with background, or misses the words you actually type, the agent can walk past it, and the more skills there are, the more descriptions compete for the same request.
TL;DR: Before a skill loads, the agent has only its description to go on, and each listing entry is cut at 1,536 characters by default. Lead with a clear "Use when" trigger and include verbatim user prompt phrases. Keep SKILL.md under 500 lines as a lean procedure, and move rubrics into a references/ folder.
What Claude Code skills are (and what they are not)
A skill in Claude Code is a folder containing a SKILL.md file that provides on-demand instructions, reference material, and optional scripts for a specific workflow. Unlike CLAUDE.md, which is loaded into context on every turn, a skill loads its full body only when invoked. Unlike hooks, which enforce hard boundaries on tool events, skills are guidance for the model.
Skills live in three locations according to the official Claude Code skills documentation: enterprise configuration, personal user directories (~/.claude/skills/<name>/SKILL.md), or project roots (.claude/skills/<name>/SKILL.md). Precedence runs from enterprise down to personal, then project. Plugins contribute skills namespaced as /plugin:skill, and custom commands in .claude/commands/ merge into skills as slash commands.
A skill preserves context by staying dormant on disk until a prompt matches its frontmatter trigger.
The description is the trigger: how to make an agent load a skill
The frontmatter description is the only text the model evaluates when deciding whether to activate a skill. The body of SKILL.md stays on disk until invocation occurs. If the description fails to convince the model that the skill applies, the instructions inside never run.
The combined description and when_to_use fields are cut at 1,536 characters in the skill listing by default (the skillListingMaxDescChars setting changes it). If you open with background context, the concrete trigger criteria get truncated before the model reads them. Put the trigger condition in the first sentence.
Across my 130 skills, description lengths range from 14 to 931 characters, with a median of 214. None come near the per-skill cap. The limit that matters more at that size is the listing as a whole: per the docs on descriptions being cut short, all names and descriptions share a budget of 1% of the model's context window, and when it overflows, Claude Code drops descriptions starting with the skills you invoke least. A skill you rarely use can lose its description entirely, and with it every keyword the model would have matched. /doctor estimates the listing's cost and its biggest contributors, and skillOverrides can set low-priority skills to "name-only" to free room for the rest.
The pattern I lean on most is a "Use when" clause, present in 36 descriptions. For example, a coordination skill's whole trigger is one sentence: "Use when the Marketing PL or the owner asks this session to join or reconnect."
Here is the difference on a made-up skill. Weak:
description: This skill contains our team's conventions and background on how we think about database changes.
Better:
description: Use when writing or reviewing a database migration, "add a column", "migrate the schema". Checks for a down migration and a lock-safe ALTER.
The first tells the model what the file is. The second tells it when to open it. Then add the words you actually type when you want that job done. The model is matching your request against the description, so name the task the way you ask for it, not the way you would title a design doc.
Some plugins, such as Superpowers, push agents to use skills by injecting a rule telling the agent to invoke a skill if there is even a small chance it applies. Even with that rule, the agent still has to pick the right skill, and the description is what it picks from. I have not measured whether the rule changes how often skills load.
One skill, one job: variants over conditionals
The tempting design is one big skill full of conditionals: if repo A, use this tool; if repo B, use that one. Every reader of that file, human or model, has to hold all the branches at once to follow one of them.
I split them into variant skills instead. In my daily workflow, I maintain a general project management loop skill called pm-loop, alongside a specialized variant named pm-loop-ping.
The description for pm-loop-ping states that it is identical to pm-loop except for which worker backends it may use. Instead of writing branching logic inside pm-loop, my global CLAUDE.md contains a pointer table that maps individual repositories to the correct skill variant. Neither file has to explain the other repo's workers.
The same principle applies to small procedural routines. I run tiny "join" skills, such as join-mkt-pl and join-yt-pl. Each file consists of about six numbered steps, with the exact terminal command, used to register a session with a coordinating lead session. They are small, deterministic, and I invoke them by typing the slash command.
Keep the procedure lean and push reference detail to sibling files
The docs recommend keeping SKILL.md under 500 lines. Compaction is one concrete reason: after automatic compaction, Claude Code re-attaches up to 5,000 tokens per invoked skill and 25,000 across all of them, so in a long session a very large skill does not come back whole.
Across my 130 skills, the smallest files run 13 to 19 lines, while the largest span 486 to 753 lines. Six of mine are over the 500-line guideline, up to 753 lines. Those are the ones to split next, and I have not done it yet.
The clean pattern is keeping SKILL.md as an executable checklist while offloading reference material into sibling directories. In my setup, 53 skills use a references/ folder, and 15 skills include a scripts/ folder.
A clear example is upwork-pipeline, a skill managing a freelance marketplace workflow. The primary SKILL.md is only 96 lines long. It outlines the end-to-end procedure, but delegates reference data to two files inside references/: an 82-line job fit rubric and a 60-line reviewer prompt template.
Its description starts: "Use for Ramiz's freelance marketplace job hunt (Upwork first...)", lists what it covers, and provides explicit trigger phrases.
The procedure names each reference file at the step that needs it: the rubric at scoring, the reviewer prompt at the review handoff. That is the instruction, not a measurement of what the agent loaded when. Per the docs, files in scripts/ are run, not loaded into context.
Put non-negotiable boundaries at the top in plain "Never" lines
My rule for my own skills: if a step is critical to security, money, or process integrity, state it as an unambiguous negative constraint near the top of the file.
In upwork-pipeline, the procedure handles outgoing client communications. Near the top of the file, before any workflow stages appear, sits a hard rule: never submit a proposal, send a message, or send an email without explicit approval of that exact text.
The file also enforces an organizational boundary: "The drafter never gates its own draft." The proposal draft must be handed to an independent reviewer session, capped at a maximum of two revision rounds.
When an autonomous agent drafts something, letting it approve its own output is how mistakes go out unread, the same reason an agent that says the tests passed still needs checking. Plain "Never" lines right after the frontmatter are hard to miss and hard to read as optional. A skill is still guidance, though: where a rule must hold even if the model ignores it, that is a hook's job, not a skill's.
Frontmatter options and invocation controls in practice
Beyond name and description, fields like disable-model-invocation, user-invocable, allowed-tools, and paths allow fine-grained control over execution boundaries.
The table below shows what each configuration field does and how many times it appears across my 130 active skills.
| Frontmatter field | What it does | Count in 130 skills | Practical use case |
|---|---|---|---|
description | Primary discovery string evaluated by the model during planning | 130 | Standard trigger instructions and task keywords |
allowed-tools | Grants tool execution permissions without prompting the user for that turn | 6 | Automated data gathering or trusted read-only scripts |
argument-hint | Displays parameter guidance in the slash autocomplete menu | 6 | Slash commands requiring arguments like IDs or paths |
user-invocable | false hides the skill from the slash menu so only Claude can invoke it | 2, both set to true (the default) | Background knowledge the model should load but you would never type |
disable-model-invocation: true | Prevents model auto-triggering; excludes description from context | 0 | High-impact actions like production deployments or database resets |
paths | Restricts automatic model invocation to matching file globs | 0 | Subsystem workflows like database migrations or manifest edits |
Every skill has name and description. The next most common fields are metadata (63) and license (34), and neither changes when a skill loads. The six allowed-tools skills are research and bug-scan skills, and their lists are broad (most include Bash), which is worth a second look since the grant skips permission prompts for that turn. The two user-invocable lines set the default and change nothing.
The zero counts for disable-model-invocation, paths, and context: fork are gaps in my setup, not a recommendation.
Setting disable-model-invocation: true fits high-risk tasks like production deployments or database resets: it stops Claude from invoking the skill on its own and keeps it for explicit invocation.
Similarly, paths scopes automatic activation to specific files, like running API checks only when editing src/api/**. The context: fork option runs the skill inside an isolated subagent, preventing exploratory research from cluttering primary context.
Claude Code ignores unrecognized frontmatter fields without error. If the YAML itself fails to parse, the skill loads with no fields set, so its description is ignored and Claude cannot discover it.
Skills also support argument substitution and command execution. Use $ARGUMENTS or positional variables $0 and $1 to accept slash command arguments. Prefixing a command with backticks and an exclamation mark, like !`git status --short` , executes shell commands and injects output into the skill before Claude reads it.
Managing skills across machines and profiles: link, never copy
How you distribute and store skills across your workstation matters just as much as how you write them. In my engineering environment, 130 skills live in a single shared directory on disk.
Three separate Claude Code profiles run on my system, each assigned to a different role. Two profiles were configured with symbolic links pointing directly to the shared skills folder. The third profile, however, held a plain directory copy instead of a symlink.
While counting skills for this post, I found that the copy had fallen 19 skills behind the shared folder. Two earlier commits titled "sync drifted skills" show it had happened before. Copying again does not fix it; it resets the clock. I reported it to the session that owns that profile so the copy becomes a link. Link, never copy.
To protect the collection across hardware, the shared folder is backed up in a private git repository synced between a Mac and a Windows build PC. That backup repository has accumulated 1,437 commits.
It is the same problem as the shared CPU, phones and disk in running multiple Claude Code agents on one machine: anything several sessions depend on needs exactly one source.
My global CLAUDE.md ends with a "Pointer index" table instructing the agent: "read the relevant file when the task matches, don't preload." Each row pairs a specific task trigger with a local file path.
This index mirrors the core philosophy of skills: keep the always-loaded context footprint minimal, and load detailed operational procedures only when a matching task requires them.
A minimal skill, end to end
Here is an illustrative skill, not one of mine, written against Claude Code 2.1.310. It lives at .claude/skills/release-notes/SKILL.md:
---
name: release-notes
description: Use when asked to write release notes, a changelog entry or "what changed since the last tag". Drafts notes from git history in the project's changelog style.
---
Never invent a change that is not in the commit log.
1. Find the last tag: !`git describe --tags --abbrev=0`
2. List commits since it with `git log <tag>..HEAD --oneline`.
3. Group them using the categories in [style.md](style.md).
4. Write the entry at the top of CHANGELOG.md. Do not commit.
style.md sits next to it:
Categories, in this order: Added, Changed, Fixed. One line per change,
past tense, no commit hashes. Skip merge commits and version bumps.
It assumes the repo has at least one tag. Three checks tell you it works. Typing /release-notes runs it by hand. Asking "write the changelog for this release" in plain words, without the slash, should load it on its own. And the result should be a new CHANGELOG.md entry that lists only commits since the tag, under those three headings, with nothing committed. The first two test discovery; only the third tests the skill.
Debugging a skill that refuses to fire
When a skill does not fire, check discovery before you touch the instructions:
First, run /skills in your session. If your skill is missing, verify its path: personal skills belong in ~/.claude/skills/<name>/SKILL.md and project skills in .claude/skills/<name>/SKILL.md.
Second, check YAML syntax. The opening --- must be the first line of the file, and the YAML must parse, or the description is ignored.
Third, check the listing budget. If /doctor shows the listing over budget, your description may have been dropped for being rarely used, and rewriting it will not help; trim other descriptions or set some to "name-only".
Fourth, test direct invocation by typing /<name>. If it runs by hand but never on its own, the description is the first suspect, though not the only one: check the next two steps too.
Fifth, check for naming collisions. Precedence runs enterprise, personal, then project. If a project skill shares a name with a personal skill, the personal version wins.
Sixth, check invocation controls. disable-model-invocation: true or a paths glob that does not match the files you are editing will keep a skill from firing automatically, by design.
Seventh, rewrite the description: trigger condition first, then the words you type for that task.
What to set up first
If your agent is ignoring skills in your repository, focus on the discovery layer first:
- Rewrite your frontmatter descriptions so the first sentence states the explicit trigger condition, starting with "Use when" and incorporating the vocabulary you type into prompts.
- Audit your file sizes and refactor any
SKILL.mdover 500 lines by moving reference tables, rubrics, and prompts into areferences/subdirectory. - Consolidate your skills into a single directory backed by a private git repository, and connect your Claude profiles using symbolic links rather than folder copies.
None of this makes a skill a guarantee. It gives the agent a fair chance to find it and a short procedure to follow once it does.
Keep reading
- Twelve Cores, Seven Devices, One DiskGit worktrees keep parallel agents out of each other's files. They do nothing about the CPU, the test phones, the disk or the run that hung seven hours ago. Those are the parts that actually broke.
- When Your Coding Agent Hits a Rate Limit: A Fallback Ladder That Keeps WorkingA rate limit, a stalled run that already finished the work, and a pull request that still fails review need three different responses. Here is the ladder I use to tell them apart.
- Don't Trust an Agent That Says the Tests PassedAn agent's report is a claim. The setup I run only accepts evidence a machine or a second agent can check, and the cases where that caught something are on the page.