Build Custom Claude Code Skills
What you're building
A Claude Code skill is a set of instructions that Claude loads and follows when it encounters a matching task. Skills turn tribal knowledge into repeatable procedures. Instead of explaining your testing conventions or deployment process every session, a skill encodes them permanently.
This tutorial walks through building three skills of increasing complexity. By the end, you'll understand the structure, the iteration process, and the patterns that separate useful skills from ones Claude ignores. If you're unfamiliar with what skills are conceptually, read our skills overview first.
The minimum viable skill
Every skill needs exactly one file: SKILL.md. It lives in a directory under .claude/skills/. The structure is simple:
.claude/skills/your-skill-name/
└── SKILL.md
The SKILL.md file has two parts: YAML frontmatter (metadata Claude uses to decide when to load the skill) and markdown content (the instructions Claude follows).
Here's the smallest possible skill:
---
name: run-tests
description: Run the project test suite and report results. Use when asked to test, run tests, or verify changes.
---
## Instructions
1. Run `pnpm test` from the project root
2. If tests pass, report the count and confirm all green
3. If tests fail, list each failure with the file path and error message
4. Never modify test files to make failing tests pass - report the failures
That's it. Create the directory, write the file, and the skill is active immediately. No restart, no registration, no build step. Claude watches the skills directory and picks up changes in real time.
Frontmatter rules
Two fields are required:
- name: a human-readable identifier (64 characters max). This becomes the skill's identity in Claude's context. Keep it descriptive but short.
- description: tells Claude when to invoke the skill (200 characters max). This is the most important field. Claude matches incoming requests against skill descriptions to decide which skills to load.
A vague description like "helps with testing" means Claude might not load the skill when you say "run the unit tests." A specific description like "Run the project test suite and report results. Use when asked to test, run tests, or verify changes" gives Claude clear trigger conditions.
Write the description as if you're telling a colleague exactly when to pull out this specific checklist.
Skill 1: A code review checklist
Let's build a practical skill. This one gives Claude a consistent code review process.
Create .claude/skills/code-review/SKILL.md:
---
name: code-review
description: Review code changes for quality, security, and consistency. Use when asked to review code, check a PR, or audit changes.
---
## Review process
When reviewing code, follow these steps in order. Don't skip steps even if the changes look simple.
### 1. Understand the change
- Read the diff completely before commenting
- Identify which files are modified and what the change intends to do
- Check if there's a related issue or PR description for context
### 2. Correctness check
- Does the code do what it claims to do?
- Are there edge cases the author hasn't handled?
- Are error paths handled properly (not just the happy path)?
- Do any existing tests need to be updated?
### 3. Security scan
- No hardcoded secrets, API keys, or credentials
- No SQL injection vectors (raw string concatenation in queries)
- No unvalidated user input reaching sensitive operations
- No new dependencies with known vulnerabilities
### 4. Style and consistency
- Follows the project's naming conventions
- No commented-out code without explanation
- Functions are reasonably sized (under 50 lines preferred)
- Types are explicit, not inferred, at function boundaries
### 5. Report format
Present findings as:
- **Must fix**: Issues that would cause bugs or security problems
- **Should fix**: Style violations or maintainability concerns
- **Consider**: Suggestions that are opinion-based or optional
If there are no issues, say so explicitly. Don't manufacture concerns to justify the review.
This skill works because it gives Claude a concrete procedure with specific criteria. Without it, Claude's code reviews are ad-hoc, it checks different things each time. With it, every review follows the same thorough process.
Testing your skill
Here's the iteration loop that makes skills effective:
- Write the initial skill
- Trigger it by asking Claude something that matches the description
- Observe where Claude deviates from what you wanted
- Refine the SKILL.md to close the gaps
- Repeat until the output is consistently right
For the code review skill, test it:
claude "Review the changes in the last commit"
Watch what Claude does. Does it follow all five steps? Does it skip the security scan on a frontend change? Does it manufacture fake concerns? Each observation becomes a refinement to the skill.
Common refinements after first tests:
- Claude skips steps for "simple" changes → Add: "Don't skip steps even if the changes look simple"
- Claude is too verbose → Add: "Keep each finding to 1-2 sentences"
- Claude misses your project's specific patterns → Add project-specific rules to the style section
Most skills reach a good state within two or three iterations. The first draft is rarely the final version, and that's fine.
Skill 2: A feature scaffolding skill with templates
Skills can include supporting files, scripts, templates, reference documents, alongside the SKILL.md. This makes them powerful for scaffolding workflows.
Create a skill that generates new API endpoints with your team's conventions:
.claude/skills/new-api-endpoint/
├── SKILL.md
└── templates/
└── endpoint-template.ts
The template file (.claude/skills/new-api-endpoint/templates/endpoint-template.ts):
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { TRPCError } from "@trpc/server";
// Input validation schema
const {{name}}Input = z.object({
// Define input fields
});
// Output type
const {{name}}Output = z.object({
// Define output fields
});
export const {{name}}Router = createTRPCRouter({
get: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
// Implementation
}),
create: protectedProcedure
.input({{name}}Input)
.mutation(async ({ ctx, input }) => {
// Implementation
}),
});
The SKILL.md (.claude/skills/new-api-endpoint/SKILL.md):
---
name: new-api-endpoint
description: Scaffold a new tRPC API endpoint with standard patterns. Use when asked to create a new endpoint, new route, or new API.
---
## Scaffolding procedure
When creating a new API endpoint, follow these steps:
### 1. Gather requirements
Ask for (if not already provided):
- Endpoint name (singular noun, e.g., "invoice", "project")
- Operations needed (CRUD or subset)
- Key data fields
### 2. Generate files
Use the template in `templates/endpoint-template.ts` as the base structure. Replace `{{name}}` placeholders with the actual endpoint name.
Create:
- `src/server/api/routers/{name}.ts` - the router file from template
- `src/server/api/routers/{name}.test.ts` - tests for each operation
### 3. Register the router
Add the new router to `src/server/api/root.ts`:
- Import the router
- Add it to the `appRouter` definition
### 4. Add input validation
Define Zod schemas for every input. Rules:
- All string inputs get `.trim()` and `.min(1)`
- All IDs use `.uuid()` or `.cuid()`
- Never use `.any()` or `.unknown()`
### 5. Implement database operations
- Use the repository pattern in `src/server/db/repositories/`
- Create a new repository file if one doesn't exist for this entity
- Never write raw SQL in router files
### 6. Verify
- Run `pnpm typecheck`
- Run `pnpm test:unit -- --filter {name}`
- Verify the router is registered by checking `root.ts`
This skill combines procedural instructions with a concrete template file. When Claude scaffolds a new endpoint, it follows the same pattern every time, using the same template, applying the same validation rules. New team members get the same output as senior developers.
Skill 3: A deployment skill with scripts
For complex workflows, skills can include executable scripts. This is where skills become genuinely powerful, they can orchestrate multi-step processes with real verification.
.claude/skills/deploy/
├── SKILL.md
└── scripts/
└── verify-deploy.sh
The verification script (.claude/skills/deploy/scripts/verify-deploy.sh):
#!/bin/bash
# Verify a deployment is healthy
ENV=$1
if [ -z "$ENV" ]; then
echo "Usage: verify-deploy.sh <environment>"
exit 1
fi
case $ENV in
staging)
URL="https://staging.example.com"
;;
production)
URL="https://example.com"
;;
*)
echo "Unknown environment: $ENV"
exit 1
;;
esac
# Check health endpoint
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL/api/health")
if [ "$STATUS" != "200" ]; then
echo "FAIL: Health check returned $STATUS"
exit 1
fi
echo "OK: $ENV deployment healthy at $URL"
The SKILL.md:
---
name: deploy
description: Deploy the application to staging or production. Use when asked to deploy, ship, release, or push to an environment.
---
## Deployment procedure
### Pre-deploy checks (mandatory)
1. Confirm the target environment (staging or production)
2. If production, require explicit user confirmation - never deploy to production without it
3. Run `pnpm typecheck` - abort on failure
4. Run `pnpm test:unit` - abort on failure
5. Run `pnpm build` - abort on failure
### Deploy to staging
1. Run `git push origin HEAD:staging`
2. Wait 30 seconds for CI/CD to pick up the push
3. Run `.claude/skills/deploy/scripts/verify-deploy.sh staging`
4. Report success or failure
### Deploy to production
1. Confirm current branch is `main`
2. Confirm all CI checks passed on the latest commit
3. Run `git tag v$(date +%Y%m%d.%H%M) && git push --tags`
4. Wait 60 seconds for CI/CD
5. Run `.claude/skills/deploy/scripts/verify-deploy.sh production`
6. If verification fails, immediately report the failure - do not attempt to fix it
### Post-deploy
- Report the deployment status, environment URL, and git tag
- If any step failed, report exactly which step and the error output
Make the script executable:
chmod +x .claude/skills/deploy/scripts/verify-deploy.sh
This skill demonstrates the full power of the system: structured instructions with embedded script execution. Claude follows the procedure, runs the scripts, and reports results. The production safety gate (explicit confirmation required) is encoded in the skill, not left to memory.
Writing effective skill instructions
After building several skills, patterns emerge for what works:
Be imperative, not descriptive. Say "Run pnpm test" not "You might want to run the tests." Claude follows instructions better when they're direct commands.
Number your steps. Numbered lists create a clear sequence. Claude is less likely to skip or reorder numbered steps than bullet points.
Include failure handling. For each step that can fail, tell Claude what to do when it fails. "Run typecheck, abort on failure" is clear. Without it, Claude may try to proceed or fix the error itself.
State what not to do. Negative constraints are as valuable as positive instructions. "Never modify test files to make failing tests pass" prevents a specific behavior Claude sometimes exhibits.
Keep it under 100 lines. Skills load into Claude's context window. A 500-line skill consumes context that could be used for actual work. If your skill exceeds 100 lines, consider splitting it into multiple focused skills.
Reference file paths explicitly. "Check the config file" is vague. "Check src/config/app.config.ts" is precise. Claude navigates faster with exact paths.
Distributing skills across your team
Skills committed to .claude/skills/ in your repository automatically distribute to every developer who clones or pulls. This is the simplest distribution method and works well for project-specific skills.
For personal skills that you use across multiple projects, place them in ~/.claude/skills/. These load for every project you work on but aren't shared with your team.
For organization-wide skills (coding standards, security review procedures, deployment protocols), maintain a shared skills repository and symlink or copy into each project. Some teams use a git submodule:
git submodule add git@github.com:your-org/claude-skills.git .claude/skills/shared
This gives you version-controlled, updatable skills that stay consistent across repositories. When the shared skills repo updates, each project pulls the changes.
For more on team-wide Claude Code configuration, including how skills fit into the broader setup, see our teams guide.
Skills vs hooks vs CLAUDE.md
Each configuration layer has a distinct role. Using the wrong one leads to frustration:
| Use case | Right tool | Why |
|---|---|---|
| "Always format code with Prettier after edits" | Hook | Deterministic enforcement, no exceptions |
| "Our API uses tRPC with Zod validation" | CLAUDE.md | General context Claude needs everywhere |
| "When deploying, follow these 8 steps exactly" | Skill | Procedural workflow triggered on demand |
| "Never modify .env files" | Hook | Hard security boundary |
| "We use conventional commits" | CLAUDE.md | Convention that applies to all work |
| "Review code using this 5-step checklist" | Skill | Structured procedure for a specific task |
The rule of thumb: if it's a constraint, use a hook. If it's context, use CLAUDE.md. If it's a procedure, use a skill.
Debugging skills that don't trigger
The most common problem with new skills is Claude not loading them when expected. Here's the debugging checklist:
Check the description. Claude matches your request against skill descriptions. If you ask "deploy to staging" but the description says "push code to servers," the match may not trigger. Use the same vocabulary your team naturally uses.
Check the file location. Skills must be in .claude/skills/your-skill/SKILL.md (project-level) or ~/.claude/skills/your-skill/SKILL.md (personal). A SKILL.md in the wrong directory is invisible.
Check the frontmatter format. YAML frontmatter must have --- delimiters, and both name and description fields. Missing or malformed frontmatter makes the skill invisible.
Check file naming. The file must be named SKILL.md (uppercase). skill.md or Skill.md won't be recognized.
Test with explicit invocation. If automatic matching isn't working, try asking Claude directly: "Use the deploy skill to deploy to staging." This confirms the skill loads correctly and narrows the problem to the matching logic.
Start building
Pick one workflow you repeat frequently, running tests, reviewing code, setting up a new component, and encode it as a skill. Follow the write-test-refine loop until Claude handles it consistently.
Skills compound over time. Each one you build removes a manual explanation from your workflow. After a dozen well-crafted skills, Claude Code stops feeling like a general-purpose assistant and starts feeling like a teammate who knows your codebase, your conventions, and your processes.
Don't want to build from scratch? Claudify ships with a library of production-tested skills covering code review, testing, deployment, documentation, and more, ready to customize for your project. Each skill has been through dozens of iteration cycles so you can skip the refinement phase.
Frequently asked questions
Can a skill call other skills?
Not directly. Skills are loaded into Claude's context as instructions, they don't have a built-in mechanism to invoke each other. However, Claude can follow a skill's instructions that reference another skill's domain. For example, a deployment skill can say "Run the test suite using the project's standard process," and if a testing skill is loaded, Claude will follow both. In practice, this works well for composing workflows.
How many skills can I have active at once?
There's no hard limit on the number of skills in your .claude/skills/ directory. However, Claude only loads skills relevant to the current task, it doesn't load all skills into context simultaneously. The practical limit is determined by the combined size of loaded skills fitting within the context window. Keep individual skills focused and under 100 lines to maximize how many can coexist.
Do skills work with Claude Code subagents?
Yes. When Claude spawns a subagent, the subagent inherits the same skills directory. If a subagent encounters a task matching a skill's description, it loads and follows that skill. This means your skills apply consistently whether Claude handles a task directly or delegates it to a subagent.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify