A quick refresher before the tutorial: an agent skill is a reusable instruction package β typically a SKILL.md file β that teaches your AI coding assistant how to handle a specific class of task. Instead of re-typing the same multi-step procedure every time you ask Claude to write a migration or draft a PR description, you encode it once, install it, and let the agent load it automatically when the work matches. Skills are different from prompts (one-off instructions) and different from MCP servers (live tools that call APIs). They are the "how-to" layer β persistent, versioned, shareable procedural knowledge.
With that out of the way, let's build one.
Why build a skill instead of just using a better prompt?
Every project has repeated tasks: writing commit messages that follow a specific format, drafting PR descriptions, running a pre-release checklist, setting up feature flags. You could prompt your way through each of these every time. You could paste snippets from a notes file. But neither approach is durable, shareable, or consistent across sessions.
A skill solves all three problems:
- Durable: the file lives in your repo; it does not disappear when you close the chat.
- Shareable: teammates install the same skill and get the same behavior.
- Consistent: the agent follows the same procedure each time the trigger fires.
The ROI compounds. A skill that saves you five minutes per sprint is already worth the 30 minutes to write it. One that your whole team uses saves hours per sprint.
The SKILL.md format
A valid SKILL.md has two parts: a YAML frontmatter block and a Markdown body.
Required frontmatter fields
---
name: write-git-commit-message
description: >-
Use this skill when the user asks to write, draft, or generate a git commit
message, or when the user says "commit" and provides staged changes.
version: 1.0.0
---
The fields:
| Field | Required | Purpose |
|---|---|---|
name | Yes | Unique identifier; becomes the install slug |
description | Yes | What the agent reads to decide whether to load this skill |
version | Yes | Semantic version; required for registry publishing |
author | Recommended | Your name or GitHub handle |
tags | Recommended | Helps discovery in registries like ExplainX |
tools | Optional | Restrict which tools the agent may use when this skill is active |
What makes a trigger condition good vs bad
The description field is doing double duty. Humans read it; the agent uses it to decide when to load the skill. Most skill authors get this wrong.
Bad trigger condition:
description: Helps with git.
This is too vague. The agent might load it for any git-related question, or miss the actual trigger.
Also bad:
description: >-
Use this skill for writing excellent commit messages that follow the
conventional commits specification and capture the essence of the change
in a way that will be useful to future readers of the git log.
This is a marketing pitch, not a trigger. It describes what the skill does, not when to use it.
Good trigger condition:
description: >-
Use this skill when the user asks to write, draft, or generate a git commit
message. Also use when the user says "commit" and staged changes are visible,
or when the user asks to improve an existing commit message.
The pattern: start with "Use this skill when..." and list specific user phrasings and context signals. Think about the actual words a developer would type when they want this capability.
The body structure
The body follows a consistent structure that teams converge on because it works:
## Purpose
One short paragraph: what this skill does and when it applies.
## Context
What the agent needs to know before starting. Tech stack assumptions,
team conventions, project-specific rules.
## Steps
1. First action the agent takes.
2. Second action.
3. Third action.
- Sub-step for edge case A
- Sub-step for edge case B
## Examples
### Good output example
[A complete example of what success looks like]
### Bad output example
[A complete example of what to avoid]
## Output format
Describe the exact format: length limits, required sections, prohibited content.
The Examples section is not optional if you want reliability. Models calibrate quality from examples far better than from abstract descriptions.
Choosing what to build a skill for
Before you open a text editor, spend two minutes answering these questions:
Good skill candidates:
- Do I do this task more than once per sprint?
- Does the task follow the same procedure most of the time?
- Do I have to explain the same context to the agent every time I do this?
- Would a new team member benefit from this procedure?
If yes to two or more: build the skill.
Examples of good skill candidates:
- Write a database migration (follows a consistent schema pattern)
- Draft a PR description (same structure: summary, test plan, breaking changes)
- Add a feature flag (your team uses one specific flag library and convention)
- Run the release checklist (same steps every time)
- Create an API endpoint (follows your project's controller/service/schema pattern)
Bad skill candidates:
- One-off refactors (by definition, not repeatable)
- Debugging a specific bug (context-specific; can't generalize)
- Tasks that change every sprint (the skill will be stale immediately)
- Tasks that need live data (use MCP servers instead)
- Tasks with no consistent procedure (judgment calls don't compress well into skills)
Step-by-step: building a "Write Git Commit Message" skill
This is a real, useful skill you can install today. We'll build it from scratch.
Step 1: Create the skill directory
In your project root:
mkdir -p .claude/skills/write-git-commit-message
touch .claude/skills/write-git-commit-message/SKILL.md
Claude Code discovers skills by scanning .claude/skills/ for folders containing a SKILL.md file.
Step 2: Write the skill file
---
name: write-git-commit-message
description: >-
Use this skill when the user asks to write, draft, or generate a git commit
message, or when the user says "commit" and staged changes are available,
or when the user asks to improve an existing commit message.
version: 1.0.0
author: your-name
tags:
- git
- workflow
- commit
---
## Purpose
Write a high-quality git commit message that follows the Conventional Commits
specification. The commit message should communicate what changed and why β
not just a description of the diff.
## Context
- Use the Conventional Commits format: `type(scope): subject`
- Valid types: feat, fix, docs, style, refactor, perf, test, chore, ci
- Subject line: 72 characters maximum, imperative mood, no trailing period
- Body (optional): explain the WHY, not the WHAT; wrap at 72 characters
- Breaking changes: append `!` after the type and add a BREAKING CHANGE footer
- This project uses English for all commit messages
## Steps
1. Read the staged diff (`git diff --staged` or the changes the user has shared).
2. Identify the primary intent of the change: is it a new feature, a bug fix,
a refactor, or something else?
3. Identify the scope if obvious (the module, file, or subsystem affected).
4. Draft the subject line: `type(scope): verb + what changed`.
5. Decide if a body is needed. A body is needed when:
- The why is not obvious from the subject
- There are multiple related changes worth explaining separately
- The change has implications beyond what the diff shows
6. Draft the body if needed. Start with a blank line after the subject.
Focus on motivation and context, not on restating the diff.
7. Add a BREAKING CHANGE footer if the change breaks an existing API or behavior.
8. Present the final message in a code block so the user can copy it.
## Examples
### Good commit β simple
fix(auth): prevent token expiry during active sessions
Tokens were expiring mid-session when background jobs ran longer than the token lifetime. Added a sliding window refresh that extends expiry on each authenticated request.
### Good commit β breaking change
feat(api)!: replace paginated /users endpoint with cursor-based pagination
BREAKING CHANGE: The ?page= and ?per_page= query parameters are removed. Clients must migrate to ?cursor= and ?limit=. See MIGRATION.md for details.
### Bad commit
update stuff
Fixed the bug that was causing issues with the thing that broke.
## Output format
Return only the commit message inside a markdown code block with no shell
language tag. Do not add explanatory text before or after the code block
unless the user asks for an explanation. If multiple valid commit messages
are possible, present one primary recommendation and note alternatives below.
Step 3: Test it locally in Claude Code
Open Claude Code in your project directory and type:
Write a commit message for my staged changes.
Watch the output. If the skill triggers, you should see Claude following the structured procedure. If it doesn't trigger, check that the .claude/skills/ directory is in the right location and the frontmatter is valid YAML.
Step 4: Verify the trigger fires
Try different phrasings:
- "commit this" β should fire
- "generate a commit" β should fire
- "how do I write better commits?" β should probably NOT fire (this is a question, not a task request)
- "update the changelog" β should NOT fire
If triggers are misfiring, tighten the description. Add phrases like "Do NOT use this skill when the user is asking a general question about commits" if you need to exclude common mismatches.
Step 5: Refine based on results
The first version of any skill is a hypothesis. After a few uses, you'll notice:
- Output format inconsistencies (tighten the output format section)
- Missing edge cases (add to the steps or examples)
- Trigger misfires (add exclusions to the description)
- Steps that the agent skips (make them more explicit with numbered sub-steps)
Iterate. Version 1.0.1 is always better than 1.0.0.
Step 6: Package for sharing
Add a README.md to the skill folder explaining what the skill does, what conventions it assumes, and how to customize it. This is what other developers will read before installing.
.claude/skills/write-git-commit-message/
βββ SKILL.md # The skill itself
βββ README.md # Human-readable documentation
The full SKILL.md spec reference
Here is a complete example showing every field and section:
---
name: add-feature-flag
description: >-
Use this skill when the user asks to add, create, or implement a feature flag,
toggle, or feature switch. Also use when the user says "gate this behind a
flag" or "make this conditional on a flag."
version: 1.2.0
author: your-team
tags:
- feature-flags
- launchdarkly
- workflow
tools:
allow:
- Read
- Edit
- Bash
---
## Purpose
Add a feature flag using LaunchDarkly to gate new functionality. Follows
the team convention of SDK-level evaluation with server-side defaults.
## Context
- Flag library: LaunchDarkly Node.js SDK v7+
- Flag keys follow snake_case: `enable_new_checkout_flow`
- Default variation is always `false` (safe default)
- All flags must be documented in docs/feature-flags.md
- SDK client is initialized in lib/launchdarkly.ts
## Steps
1. Ask the user for the flag key if not provided. Remind them to use snake_case.
2. Ask which component or route should be gated.
3. Import the LaunchDarkly client from `lib/launchdarkly.ts`.
4. Wrap the gated code with `if (await ld.variation('flag-key', context, false))`.
5. Add a comment above the flag check: `// FF: flag-key - remove after rollout`.
6. Update docs/feature-flags.md with: flag key, description, owner, and rollout target.
7. Show the user the complete diff for review.
## Examples
### Gating a React component
```tsx
// FF: enable_new_checkout - remove after 2026-Q3 rollout
if (await ld.variation('enable_new_checkout', userContext, false)) {
return <NewCheckout />;
}
return <LegacyCheckout />;
docs/feature-flags.md entry
| Flag Key | Description | Owner | Target Date |
|---|---|---|---|
| enable_new_checkout | New checkout flow | @checkout-team | 2026-Q3 |
Output format
Show changes as a unified diff. Always show the docs/feature-flags.md update. Do not proceed without confirming the flag key with the user first.
## Testing your skill
### How to test trigger conditions
Create a test script that lists the phrases you expect to trigger the skill:
Expected triggers: β "add a feature flag for the new dashboard" β "gate the payment form behind a flag" β "create a toggle for beta users"
Expected non-triggers: β "what is a feature flag?" β "remove the old feature flags" β "list all our feature flags"
Test each phrase manually and note any misfires. Adjust the description field until all expected triggers fire and non-triggers don't.
### How to verify the skill produces the right output
Run the skill against three to five representative real cases from your project. Check:
- Did it follow all the steps in order?
- Is the output format correct?
- Did it handle edge cases (missing context, ambiguous inputs)?
- Did it ask for clarification when the procedure requires it?
### Common testing patterns
**The adversarial prompt test**: give the skill an input that is close but wrong. "Update the feature flag docs" should not trigger "add-feature-flag." It should trigger a hypothetical "update-feature-flag-docs" skill or just be handled without a skill.
**The new team member test**: show the skill output to someone who has never seen the codebase. Can they follow it? Does it contain enough context, or does it assume knowledge they do not have?
**The stale context test**: try the skill after a month. Is any context in the frontmatter out of date? Is the output still correct? Skills need maintenance just like code.
## Skill file organization in a real project
your-project/ βββ .claude/ β βββ CLAUDE.md # Project-level rules and context β βββ skills/ β βββ write-git-commit-message/ β β βββ SKILL.md β β βββ README.md β βββ write-pr-description/ β β βββ SKILL.md β β βββ README.md β βββ add-database-migration/ β β βββ SKILL.md β β βββ README.md β β βββ examples/ β β βββ migration-template.sql β βββ run-release-checklist/ β βββ SKILL.md β βββ README.md
Claude Code discovers skills by walking the `.claude/skills/` directory and reading the `SKILL.md` frontmatter from each folder it finds. The `examples/` subfolder pattern is useful for skills that need reference files β the skill can point to these files and the agent can read them when needed.
**One skill per folder**: do not put multiple skills in one file or one folder. The folder name becomes part of the skill's identity and install path.
<NewsletterSignup />
## Publishing a skill to ExplainX
When your skill is solid locally, publishing to [ExplainX.ai/skills](https://explainx.ai/skills) lets the wider community install it.
**What the submission process looks like:**
1. Push your skill to a public GitHub repository with the standard folder structure.
2. Visit [explainx.ai/submit](https://explainx.ai/submit) and connect your GitHub account.
3. Select the repository and the skill folder you want to submit.
4. Fill in additional metadata: category, primary use case, compatibility notes.
5. The ExplainX team reviews for quality, security, and specification compliance.
6. Approved skills appear in the registry with an install command like:
```bash
npx skills add explainx/write-git-commit-message
Quality criteria for approval:
SKILL.mdmust validate against the current spec (version field, proper description, numbered steps)- Trigger conditions must be specific and accurate
- At least two example inputs and outputs
- No hard-coded secrets, personal API keys, or environment-specific paths
- README with installation and customization notes
- Works without modification on a fresh project clone
Advanced skill patterns
Skills that call other skills
## Steps
1. Run the add-database-migration skill to create the schema change.
2. After the migration file is created, run the write-pr-description skill
to draft the pull request.
3. Ask the user to confirm before proceeding.
This works when both skills are installed. The agent follows the chain. Use this pattern for multi-phase workflows where each phase is independently reusable.
Skills with conditional branches
## Steps
1. Check whether the project uses TypeScript or JavaScript.
- If TypeScript: follow the TypeScript branch below.
- If JavaScript: follow the JavaScript branch below.
### TypeScript branch
2a. Create the file with a .ts extension...
### JavaScript branch
2b. Create the file with a .js extension...
Conditional branches let a single skill handle multiple project configurations. Keep branches short β if the branches are more than 10 steps each, consider splitting into separate skills.
Skills for teams vs personal use
Team skills go in the project's .claude/skills/ directory and are committed to the repo. They encode team-wide conventions: your migration format, your PR template, your release process.
Personal skills go in ~/.claude/skills/ (your home directory). They apply across all your projects and encode your personal workflow preferences: how you like commits formatted, your note-taking style, your code review checklist.
Skills in both locations are available simultaneously. If a personal skill and a team skill have conflicting triggers, the team skill takes precedence in that project.
Common skill-writing mistakes (and fixes)
Mistake 1: The description is a feature pitch, not a trigger. "This skill helps you write amazing commit messages that follow industry best practices." Fix: Start with "Use this skill when..." and list actual user phrasings.
Mistake 2: No examples section. Models understand what you want from examples far better than from abstract descriptions. An output without examples will be inconsistent. Fix: Add at least two complete examples: one ideal output, one anti-pattern to avoid.
Mistake 3: Steps that are too abstract. "2. Write the migration." β What does that mean? What format? What tool? Fix: Make every step actionable and specific. If a step requires a decision, spell out the branches.
Mistake 4: The skill tries to do too many things. A skill called "manage-database" that handles migrations, seeds, rollbacks, and schema inspection will be confused about when to fire and what to do. Fix: One skill per task type. Build "add-database-migration," "rollback-database-migration," and "seed-database" as separate skills.
Mistake 5: No version field.
Without a version, you cannot pin the skill in a skills.lock.json or track changes across team members.
Fix: Always include version: 1.0.0 and increment it when you change the procedure.
Mistake 6: Hard-coded project-specific paths.
/Users/yash/myproject/migrations/ will break for every other user.
Fix: Use relative paths or describe the pattern the agent should find: "locate the migrations directory in the project root."