How to Write Claude Code Skills: A Practical Walkthrough
Claude Code skills are reusable, shareable units of behavior that you can build once and invoke across projects. If you've used Claude Code for a while, you've already consumed skills without necessarily thinking about them: commands like /review, /init, or /security-review are all skills. They're not magic built into the core Claude binary. They're defined in skill files that the use knows how to load and execute.
This guide walks through building a skill from scratch. By the end, you'll have a working example and a solid understanding of how the pieces fit together.
What a skill actually is
Before diving into code, it helps to have a clear picture of what a skill is in the Claude Code architecture.
A skill is a YAML or MDX file that defines:
- A trigger description (when Claude should use this skill)
- A system prompt or set of instructions that run when the skill activates
- Optional parameters the skill accepts
- Optional tool permissions the skill needs
When you invoke a skill, the use injects the skill's instructions into the conversation alongside your request. Claude then follows those instructions rather than its default behavior.
This is why skills are so useful. You're not writing code that runs directly. You're writing structured instructions that shape Claude's behavior for a specific, repeatable task. The model does the work; the skill just focuses it.
Setting up the environment
Skills live in ~/.claude/skills/ for user-level skills, or in .claude/skills/ at the project root for project-level skills. Project-level skills are scoped to that project and checked into source control, which makes them shareable with your team.
The directory structure you need:
.claude/
skills/
my-skill/
skill.yml
README.md (optional but helpful)
You can also write skills as single .md files rather than directories:
.claude/
skills/
my-skill.md
For simple skills, the single file approach is cleaner. For skills with multiple components or that need their own documentation, use the directory structure.
The simplest possible skill
Let's build a skill that generates a changelog entry from a git diff. It's a real task developers do repeatedly, it's annoying to do manually, and it's a good fit for a skill.
Create .claude/skills/generate-changelog.md:
---
name: generate-changelog
description: Generate a changelog entry from recent git commits. Use when the user asks to create a changelog, write release notes, or document what changed.
---
You are generating a changelog entry. Follow these steps exactly:
1. Run `git log --oneline -20` to see recent commits
2. Run `git diff HEAD~10..HEAD --stat` to see what files changed
3. Group the changes into categories: Features, Bug Fixes, Breaking Changes, Internal
4. Write a changelog entry in Keep a Changelog format (https://keepachangelog.com)
5. Use past tense for all entries ("Added X", "Fixed Y", "Changed Z")
6. Do not include commit hashes in the output
7. Ask the user what version number and date to use if they haven't specified
Output only the formatted changelog section, no explanation around it.
That's a complete working skill. The frontmatter defines the name and the description that Claude uses to decide when to invoke it. The body is the instruction set Claude follows.
To use it:
/generate-changelog
Or just ask naturally:
Can you write changelog entries for this release?
Claude will recognize the task matches the skill description and invoke it.
A more complete skill: database schema reviewer
Let's build something more involved. This skill reviews a database schema file and reports potential issues. It's more useful than the changelog example because it does something that genuinely requires specialized context.
Create .claude/skills/review-schema/skill.yml:
name: review-schema
description: >
Review a database schema file for potential issues. Use when the user
asks to review, audit, or check a database schema. Works with SQL files,
Drizzle ORM schemas, Prisma schemas, and similar formats.
parameters:
- name: file
description: Path to the schema file to review. If not provided, ask the user.
required: false
type: string
instructions: |
You are reviewing a database schema. Your job is to identify real problems,
not style issues. Focus on:
**Performance risks:**
- Missing indexes on foreign keys
- Missing indexes on columns commonly used in WHERE clauses (look for _id, _at, status, type suffixes)
- N+1 query risks in the relationship design
- Tables likely to grow large with no partitioning strategy
**Data integrity risks:**
- Missing NOT NULL constraints where they're probably needed
- Missing unique constraints that the business logic probably requires
- Cascade delete rules that could cause silent data loss
- Missing check constraints on columns with known valid value ranges
**Migration concerns:**
- Column type choices that will be painful to change later (avoid text for structured data)
- Missing created_at/updated_at on tables that should have them
- No soft delete strategy where one is probably needed
For each issue, explain:
1. What the problem is
2. Why it matters in production
3. The specific fix (write the SQL or config change)
Be direct. Skip issues that are speculative. Only report real risks.
If the user hasn't specified a file, ask: "Which schema file should I review?"
This skill is more structured because it has a parameter definition and it explicitly calls out what to look for and how to present findings.
Invoke it as:
/review-schema file:src/db/schema.ts
Or:
Can you review the schema in src/db/schema.ts?
Adding tool permissions to skills
Some skills need permissions to run commands. By default, skills operate within whatever permissions are configured in your settings.json. But you can declare that a skill needs specific permissions:
name: run-tests-focused
description: Run tests for a specific module and report failures with context.
permissions:
allow:
- "Bash(npm run test:*)"
- "Bash(npx vitest run *)"
instructions: |
Run the tests for the specified module. Steps:
1. Identify which test files correspond to the module the user mentioned
2. Run those specific tests with: `npx vitest run {test-files}`
3. If tests fail, read the failure output carefully
4. For each failure, read the relevant source file to understand the context
5. Report: which tests failed, why they failed, and your assessment of
whether the issue is in the test or the implementation
The permissions.allow block adds these Bash patterns to the allowed list for this skill's execution context. The user still sees permission prompts unless they've pre-approved these patterns in settings.
Parameterized skills with defaults
Parameters make skills more flexible without making them complicated to use:
---
name: add-tests
description: >
Add missing tests to a file or function. Use when the user asks to
write tests, add test coverage, or test a specific function.
parameters:
- name: file
description: The source file to add tests for
required: true
type: string
- name: framework
description: Test framework to use (vitest, jest, pytest)
required: false
default: auto-detect
type: string
- name: coverage_target
description: Target coverage percentage to aim for
required: false
default: "80"
type: string
---
Add tests for the specified file.
Framework: {{framework}} (if auto-detect, look at package.json or requirements.txt to determine the test framework in use)
Steps:
1. Read {{file}} and identify all exported functions and classes
2. Check if a corresponding test file exists (look for *.test.ts, *.spec.ts, test_*.py patterns)
3. If no test file exists, create one following the project's naming convention
4. For each exported function, write tests that cover: happy path, error cases, edge cases (empty input, boundary values)
5. Aim for {{coverage_target}}% coverage of the functions in the file
6. Use existing test helpers and fixtures from the project if they exist
Write only the tests, not the implementation.
The {{parameter}} syntax in the instructions references the parameter values. This lets you write instructions that adapt to what the user passes in.
Sharing skills across a team
Project-level skills in .claude/skills/ get checked into version control. That's the main mechanism for sharing them.
A few things make team-shared skills work better:
Document the trigger clearly. The description field determines when Claude auto-invokes the skill. Make it precise. "Use when the user asks to review code" is too broad. "Use when the user asks to review a pull request or code change for security issues" is better.
Write the instructions for your codebase. Generic skills are useful. Skills tailored to your stack and conventions are much more useful. Reference your actual file paths, your actual error handling patterns, your actual test helpers.
Version them. Add a version comment at the top of your skill file when you make significant changes. It helps when debugging why a skill behaves differently than expected.
Test new skills explicitly. Before committing a skill, invoke it a few times with different inputs. Skills that work perfectly on one type of input often fail on variations you didn't consider.
Debugging a skill that isn't working
When a skill produces unexpected results, the most common issues are:
The description doesn't match how users phrase the request. Claude uses the description to decide when to invoke the skill. If your description says "use when reviewing database schemas" but your team types "check my migrations," the skill won't trigger automatically. Either update the description or invoke explicitly with /skill-name.
The instructions are too long. Very long instruction sets dilute attention. The model won't follow 50 bullet points with equal care. Prioritize ruthlessly. Keep instructions under 30 lines if possible.
Missing context. If the skill needs to know about your project structure but the instructions don't tell it where to look, it'll make something up. Add explicit paths and patterns.
Conflicting instructions in CLAUDE.md. If your CLAUDE.md says "never write comments" and your skill says "add JSDoc to everything," the conflict produces inconsistent behavior. Make sure your skills and your CLAUDE.md are aligned.
What makes a good skill candidate
Not every task benefits from a skill. The cases where skills really pay off:
- Tasks you do more than a few times a week
- Tasks with specific quality standards that are hard to describe quickly in a prompt
- Tasks where the output format matters (changelogs, PR descriptions, architecture docs)
- Tasks that require a consistent sequence of tool calls
- Tasks where you want to share a specific approach with your team
Tasks that probably don't need a skill: anything you can describe adequately in two sentences, anything highly specific to one unique codebase situation, anything where you want creative flexibility rather than consistent output.
The official Claude Code skills documentation has the full YAML schema reference, including all available parameter types and permission patterns. The anthropic-skills repository has the source for built-in skills like /review and /init, which are worth reading to see how Anthropic structures more complex skills.