Skills are the most underrated feature in Claude Code. I use dozens of them covering everything from Git commits to blog article creation. Most developers have none, or one or two copied from a tutorial.
This article shows how to write skills that work in production: the structure, the loading mechanism, the patterns I've identified after months of iteration, and the common mistakes.
What a skill is
A skill is a folder containing a SKILL.md file. The file combines a YAML frontmatter (metadata) and a Markdown body (instructions). When the user types /skill-name or phrases a request that matches the description, Claude loads the instructions and follows them. The official documentation covers installation and basic syntax.
commit-push/
SKILL.md # required: metadata + instructions
references/ # optional: detailed docs
scripts/ # optional: executable code
assets/ # optional: templates, static files
The SKILL.md format is an open standard created by Anthropic and adopted by other agent products like Cursor. A skill written for Claude Code works as-is in these tools.
Progressive disclosure: why it matters
The loading mechanism is the most important aspect to understand. Without it, you write skills that are too large and waste context, or too vague and never trigger.
Loading happens in three levels:
Level 1 - Discovery (~100 tokens per skill). Only the name and description from the frontmatter are injected into the system prompt at the start of each session. Claude knows the skill exists and when it applies. Even with dozens of active skills, that's only a few thousand tokens - negligible in a 200K context.
Level 2 - Activation (<5,000 tokens). When the user's request matches a skill's description, Claude reads the full SKILL.md body. This is where the detailed instructions, step-by-step workflows, and checklists live.
Level 3 - Execution (on demand, unlimited size). The agent reads files from the references/ folder or runs scripts from scripts/ only when the Level 2 instructions tell it to. A blog skill might have 20 reference files, but Claude only loads 2 or 3 per execution.
The practical consequence: the SKILL.md body should stay under 5,000 tokens. Anything beyond that belongs in references/. If the skill is too large, Claude loads thousands of context tokens every activation, even when it only needs a fraction.
Anatomy of a SKILL.md
Here's the minimal structure:
---
name: my-skill
description: "|"
What this skill does AND when to use it.
Include trigger phrases in the languages your users speak.
---
# My Skill
## Workflow
1. First step
2. Second step
3. Third step
## Output format
What the user should receive in return.
The frontmatter: the routing contract
The frontmatter is the most critical component. The description is the only text Claude sees before deciding whether to activate the skill. If it's vague, the skill won't trigger.
Technical constraints:
-
namein lowercase with hyphens only, 1-64 characters -
namemust match the parent folder name exactly - Invalid YAML silently prevents loading - no error, the skill just disappears
Here's the description from one of my production skills:
name: commit-push
description: |
Commit all changes with an auto-generated conventional commit message
and push to remote, all in one step.
Use when: "commit & push", "commit and push", "commit push",
"/commit-push", "commit et pousse", "pousse ca", "fais un commit
et push", "commit tout".
Three elements to note:
- What it does - "commit all changes with auto-generated conventional commit message and push"
- When to use it - explicit list of trigger phrases
- Bilingual - triggers in both English and French
If I had only written "Commit and push changes", the skill would trigger on "commit and push" but not on "pousse ca" or "commit tout".
The body: keep it light
The SKILL.md body contains the workflow Claude should follow. Here's a simplified excerpt from my /lint-check skill:
# Lint Check
Run the full Rust lint pipeline and auto-fix errors.
## Workflow
1. Run `cargo fmt -- --check` to detect formatting issues
2. If formatting issues found, run `cargo fmt` to fix them
3. Run `cargo clippy -- -D warnings` to detect lint issues
4. If clippy issues found, fix them one by one
5. Run `cargo check` to verify compilation
6. If errors remain after 3 fix attempts, stop and report
## Error handling
| Scenario | Action |
|----------|--------|
| cargo fmt fails | Report the error, do not continue |
| clippy warns but compiles | Fix warnings, re-run |
| cargo check fails | Show the error, suggest a fix |
The skill is 30 lines, not 300. It says what to do, in what order, and how to handle errors. Claude doesn't need an explanation of what cargo clippy is - it already knows.
Two design philosophies
Pattern A: tool wrappers
The skill is a thin wrapper around a CLI or deterministic script. The logic lives in the code; the skill just orchestrates it.
Examples from my setup:
-
/lint-checkorchestratescargo fmt,cargo clippy,cargo check -
/commit-pushorchestratesgit status,git diff,git log,git add,git commit,git push -
/seo-scanorchestrates an SEO crawler and updates a tracking file
MCP RTK is a good example of this pattern: the skill orchestrates a token filtering proxy via CLI commands, with no logic in prose.
Pattern B: cognitive disciplines
The skill encodes a methodology the agent must follow. It's pure prompt engineering - no script to run, just a thinking process.
Examples:
-
/systematic-debuggingenforces a 5-step debugging methodology (reproduce, isolate, hypothesize, verify, fix) -
/security-auditdefines an OWASP top 10 checklist with patterns to search by category
Pattern B is harder to write well. The temptation is to over-explain. Claude knows how to debug - the skill adds structure to the process, not knowledge.
Patterns for effective skills
After months of iterating, here are the patterns that work.
Bash first, prose second
A code block the agent can execute beats a paragraph describing what to do.
Bad:
Check if there are uncommitted modified files in the current
git repository by using the git status command.
Good:
1. Run `git status` to check for uncommitted changes
Claude knows what git status does. The short version is clearer and more reliable.
State-check before action
Always verify the current state before modifying anything. Without this, Claude acts on assumptions and breaks things.
## Workflow
1. Run `git status` to verify clean working tree
2. Run `git log --oneline -5` to confirm current branch
3. Only then: create the feature branch
Validation loops
After each action, verify the result is correct before moving to the next. My /dev-pipeline skill does this at every step:
4. Run `cargo clippy -- -D warnings`
5. If clippy reports errors:
a. Fix the errors
b. Re-run clippy
c. If errors persist after 3 attempts, stop and report
6. Only if clippy passes: proceed to tests
Without validation loops, Claude chains steps even when one fails. It ends up "completing" a pipeline where every step has failed.
Compose primitives
Don't bundle entire workflows into a single skill. Compose simple skills together.
My /dev-pipeline skill doesn't reimplement linting - it calls /lint-check. It doesn't reimplement commits - it calls /commit-push. Each skill does one thing, and workflows compose these primitives.
# Dev Pipeline
1. Plan the implementation (use EnterPlanMode)
2. Implement the changes
3. Run `/lint-check` to verify code quality
4. Run tests
5. Run `/commit-push` to commit and push
Document output formats
The agent must know exactly what it produces. Without an output specification, Claude improvises a different format every execution.
## Output format
Deliver a summary with:
- Files modified: list of paths
- Tests: pass/fail count
- Lint: pass/fail with details
- Commit: the conventional commit message used
Anti-patterns
Don't re-teach what the model knows
# Bad
JSON (JavaScript Object Notation) is a structured data format
used for data exchange...
# Good
Generate a JSON response matching the schema in references/schema.md.
Claude knows what JSON is. Every token wasted on unnecessary pedagogy is a context token lost.
Don't write vague descriptions
# Bad - will almost never trigger
name: helper
description: Helps with dev stuff
# Good - clear and specific triggers
name: lint-check
description: |
Run the full Rust lint pipeline (cargo fmt, clippy, check)
and auto-fix errors. Trigger on: "lint check", "lance le lint",
"cargo fmt && cargo clippy", "check my Rust code".
A vague description means Claude doesn't know when to activate the skill. It has dozens of descriptions to compare against the user's request - precision is essential.
Don't create monolithic mega-skills
One skill = one capability. If the description contains "and" between two independent actions, it's probably two skills.
My first /dev-pipeline was 500 lines with everything inline: linting, tests, review, commit, push, MR creation. Today it's 40 lines and composes five specialized skills.
Don't ignore failure modes
Document what can go wrong and how to react. Without this, Claude stops or invents a solution when a command fails.
## Error handling
| Scenario | Action |
|----------|--------|
| No git remote configured | Stop, ask user to configure |
| Pre-commit hook fails | Fix the issue, retry once |
| Push rejected (not fast-forward) | Run `git pull --rebase`, retry |
| Merge conflict after rebase | Stop, show conflicts to user |
Don't use absolute paths
# Bad
Read /Users/thomas/.claude/scripts/validate.py
# Good
Read scripts/validate.py from the skill directory
Absolute paths break when the skill is shared or used on another machine. Paths relative to the skill directory or environment variables are portable.
Project-specific skills
The most powerful skills are those adapted to a specific project. In my setup, the Netir project has six skills that encode the project's conventions:
# netir-cpm/SKILL.md (excerpt)
name: netir-cpm
description: |
Commit, push and create a GitLab Merge Request for the Netir
project. Netir conventions applied: assignee ThomasTartrau,
reviewer netir-bot, label "MR::en attente de review".
Use when: "/cpm", "create the MR", "commit push mr".
The difference from the generic /cpm: Netir conventions (labels, reviewer, assignee) are hardcoded. I don't need to re-specify them for every MR.
Another example: /netir-qa-swarm launches four review agents in parallel, each with a different focus:
## Agents
| Agent | Focus |
|-------|-------|
| Architecture | Layers, separation of concerns |
| Security | OWASP, injections, auth, rate limiting |
| Rust quality | Idioms, clippy, performance, unwrap |
| Business patterns | Domain coherence, naming, edge cases |
Each agent has instructions specific to the Netir codebase (the Axum/SQLx stack, naming conventions, error patterns). A generic review skill doesn't know these conventions.
Testing a skill
The most important test: invoke the skill with varied phrasings and verify it triggers.
Users don't say "/invoke-my-skill". They say:
- "commit and push"
- "run the linter"
- "create a MR"
- "check the code"
If these natural phrasings don't trigger the skill, the description needs work. I add failing phrasings to the frontmatter triggers.
The second test: verify the instructions produce the expected result on a real case. No imaginary dry-run - run the skill on an actual project and check the output.
Checklist before publishing a skill
Before deploying a new skill to my configuration repo:
- The
namematches the folder name exactly - The description includes triggers in French and English
- The SKILL.md body is under 5,000 tokens
- Details are in
references/, not the body - Failure modes are documented
- Output format is specified
- No secrets, tokens or absolute paths in the skill
- Destructive commands are protected by confirmations
- The skill has been tested with at least 3 different phrasings
What I've learned
The description is the router. Invest as much time on the description as on the body. A skill with perfect instructions but a vague description will never trigger.
Code beats prose. A deterministic script is always more reliable than ambiguous instructions. If a task has a single correct answer, put the logic in a script, not in Markdown.
The cheapest context is context you don't load. Progressive disclosure exists for a reason. A 200-line skill that could be 40 with references wastes context on every activation.
Test with real phrasings. Users don't type clean commands. They write "commit this", "push it", "lint" - triggers must cover these variants.
Skills transform Claude Code from a generic assistant into a tool adapted to your specific workflow. The projects page lists the other tools I've built around this ecosystem.
Top comments (0)