Tutorial · 8 min read

How to Write Your First Claude Code Skill, Step by Step

Build a real migration-review skill for Claude Code from scratch: folder setup, a description that triggers, a checklist, a bundled script, and testing.

To write a Claude Code skill, create a folder in .claude/skills/ (or ~/.claude/skills/), add a SKILL.md file with a name that matches the folder and a description that says what the skill does and when to use it, then write the procedure as concise Markdown instructions. Test it by invoking /skill-name directly and by asking a matching question without naming the skill, and refine the description and steps until both work.

This tutorial builds a real skill end to end: migration-review, which reviews database migration files for changes that lock tables or break running code. By the end you will have a skill with a tuned description, a checklist, a bundled script, and a test routine you can reuse for every skill you write.

Step 1: Decide what the skill should do

A skill should encode something Claude would not do reliably on its own. Claude knows SQL. What it does not know without being told is your bar for a safe migration and the order in which to check things.

Write the job down in two sentences before touching any files:

Review a migration before it ships. Flag any statement that takes a long lock on a large table or breaks the currently deployed code, and propose the safe alternative.

Then list what a good review always checks. For Postgres:

  1. Missing lock_timeout before DDL on existing tables
  2. CREATE INDEX without CONCURRENTLY
  3. Adding NOT NULL or a foreign key without NOT VALID then VALIDATE
  4. Column renames or drops the old code still reads
  5. Column type changes that rewrite the table
  6. Backfills done in a single unbatched UPDATE

That list is the core of the skill. Everything else is packaging.

Step 2: Create the skill folder

Make it a project skill so the whole team gets it:

mkdir -p .claude/skills/migration-review/scripts
touch .claude/skills/migration-review/SKILL.md

The folder name, migration-review, must exactly match the name field you are about to write: lowercase letters, digits, and single hyphens.

Step 3: Write a description that triggers

The description is the only part Claude sees before deciding to use the skill. A first attempt usually looks like this:

description: Helps review migrations.

That is too vague. “Migrations” could mean data migrations, cloud migrations, or framework upgrades, and nothing in it tells Claude when to reach for it. A better version names the capability, the risks, and the phrases a user would type:

description: Reviews database migration files for lock-heavy DDL and deploy-breaking changes, then proposes safe alternatives such as CREATE INDEX CONCURRENTLY, NOT VALID constraints, and expand/contract renames. Use when the user asks to review, check, or approve a migration, before merging a PR that touches migrations/, or when a migration locked a table in production. Postgres first.

It is written in the third person, front-loads the capability, and includes trigger words: review, check, approve, migration, locked a table.

Step 4: Write the instructions

Here is the complete SKILL.md:

---
name: migration-review
description: Reviews database migration files for lock-heavy DDL and deploy-breaking changes, then proposes safe alternatives such as CREATE INDEX CONCURRENTLY, NOT VALID constraints, and expand/contract renames. Use when the user asks to review, check, or approve a migration, before merging a PR that touches migrations/, or when a migration locked a table in production. Postgres first.
argument-hint: "[migration-file]"
---

# Migration review

## Changed migrations
!`git diff --name-only main...HEAD -- migrations/ || true`

## Workflow

Copy this checklist into your response and tick items as you go:

- [ ] 1. Identify target files (argument if given: $ARGUMENTS; otherwise the list above)
- [ ] 2. Run the lint script on each file
- [ ] 3. Review each statement against the rules below
- [ ] 4. Check deploy compatibility with the current code
- [ ] 5. Write the report

### Step 2: Lint
Run `bash ${CLAUDE_SKILL_DIR}/scripts/lint_migration.sh <file>`.
Treat every line it prints as a finding to confirm or dismiss with a reason.

### Step 3: Rules
| Pattern | Risk | Safe alternative |
|---|---|---|
| DDL on an existing table without `SET lock_timeout` | Queues behind long transactions and blocks all traffic | `SET lock_timeout = '5s';` first, and retry |
| `CREATE INDEX` without `CONCURRENTLY` | Blocks writes for the whole build | `CREATE INDEX CONCURRENTLY`, in its own non-transactional migration |
| `ADD CONSTRAINT ... FOREIGN KEY` or `CHECK` | Full scan under lock | Add `NOT VALID`, then `VALIDATE CONSTRAINT` separately |
| `SET NOT NULL` on a large table | Full scan under lock | Add a `CHECK (col IS NOT NULL) NOT VALID`, validate, then set NOT NULL |
| `ALTER COLUMN ... TYPE` | Possible table rewrite | New column, backfill, swap |
| Single `UPDATE` backfill | Long transaction, bloat | Batch by primary key range |

### Step 4: Deploy compatibility
For every renamed or dropped column, search the code: `grep -rn "<column>" src/`.
If the deployed code still references it, the migration must be split (expand, deploy, contract).

### Step 5: Report
Use exactly this structure:

**Verdict:** safe / safe with changes / unsafe
**Findings:** one per line: file:line, risk, fix
**Rewritten migration:** only if the verdict is not "safe"

Do not approve a migration you could not fully read. Say what you could not check.

A few deliberate choices:

  • The checklist gives Claude a visible progress tracker, which reduces skipped steps on multi-file reviews.
  • The rules table is the team’s judgment in a compact form. It does not explain what an index is.
  • The dynamic context line (!`git diff ... || true`) injects the list of changed migrations before Claude reads the skill. The || true keeps the skill loading even outside a git repo, since a failing command would otherwise abort it.
  • The fixed report format makes output consistent and easy to paste into a PR.

Step 5: Add a script for the deterministic part

Pattern matching is something a script does more reliably than a model. Create scripts/lint_migration.sh:

#!/usr/bin/env bash
# Flags common lock-heavy patterns in a SQL migration. Prints file:line: message.
set -euo pipefail
f="${1:?usage: lint_migration.sh <file.sql>}"

grep -nEi 'create( unique)? index' "$f" | grep -vi concurrently \
  | sed "s|^|$f:|; s|$|  <- index without CONCURRENTLY|" || true
grep -nEi 'add constraint .*(foreign key|check)' "$f" | grep -vi 'not valid' \
  | sed "s|^|$f:|; s|$|  <- constraint without NOT VALID|" || true
grep -nEi 'alter column .* type ' "$f" \
  | sed "s|^|$f:|; s|$|  <- type change may rewrite table|" || true
grep -nEi 'set not null' "$f" \
  | sed "s|^|$f:|; s|$|  <- SET NOT NULL scans the table|" || true
if grep -qEi 'alter table' "$f" && ! grep -qi 'lock_timeout' "$f"; then
  echo "$f:  <- ALTER TABLE without lock_timeout"
fi
chmod +x .claude/skills/migration-review/scripts/lint_migration.sh

When the skill runs, only the script’s output enters Claude’s context, not the script itself. Referencing it through ${CLAUDE_SKILL_DIR} means it works whether the skill is installed per project or personally.

Test the script on its own first:

cat > /tmp/test.sql <<'SQL'
ALTER TABLE orders ADD COLUMN region text;
CREATE INDEX idx_orders_region ON orders (region);
ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);
SQL
bash .claude/skills/migration-review/scripts/lint_migration.sh /tmp/test.sql

You should see three findings: the missing CONCURRENTLY, the missing NOT VALID, and the missing lock_timeout.

Step 6: Test that the skill triggers

Claude Code picks up new skills in .claude/skills/ without a restart. Run /skills and confirm migration-review appears.

Now test both invocation paths, each in a fresh session so earlier context does not help:

Manual invocation:

/migration-review migrations/20261012_add_region.sql

Automatic invocation, with prompts that should trigger:

Can you check the migration in this branch before I merge?
This migration locked the orders table in prod yesterday. What went wrong?
Is it safe to add a NOT NULL column to users?

And prompts that should not trigger:

Migrate this component from class syntax to hooks.
Help me plan our migration from Heroku to AWS.

Write the results down. A small table of prompt, expected, and actual is enough. If the skill fires on the Heroku prompt, the description is too broad; add “database” and “SQL” closer to the front. If it misses “is it safe to add a NOT NULL column,” add that phrasing to the trigger list.

Step 7: Watch it work and iterate

Triggering is half the job. Now give the skill real migrations, ideally including one you know is unsafe, and read the output critically:

  • Did it skip a step? Make that step more prominent, or add it to the checklist.
  • Did it explain basics at length? Add “Be terse; the reader knows SQL” to the report section.
  • Did it miss deploy compatibility? Move that check earlier, or make the grep command explicit.
  • Did it invent a finding the script did not report? That may be correct, but require it to cite file and line.

Each change takes effect on the next invocation. Two or three rounds of this usually produces a skill that behaves consistently. Then commit it:

git add .claude/skills/migration-review
git commit -m "Add migration-review skill"

Common first-skill mistakes

Most first skills fail in one of a few predictable ways. Check yours against this list before sharing it:

  • Teaching general knowledge. If the body explains what an index or a foreign key is, cut it. Claude knows. Keep the rules your team applies that a generic reviewer would not.
  • A description written for humans, not for matching. “A handy tool for our DB workflow” reads fine in a README and triggers on nothing. Name the task, the artifacts (migration files, migrations/), and the verbs people use.
  • Steps that depend on your machine. Hardcoded paths like /Users/you/project/scripts/ break for everyone else. Use ${CLAUDE_SKILL_DIR} for bundled files and ${CLAUDE_PROJECT_DIR} for repo files.
  • No definition of done. Without a fixed report format or a pass condition, Claude decides when it is finished, and that varies. The verdict line and findings list above solve this.
  • Testing only in the session where you wrote it. That session already contains the context the skill is meant to supply, so everything appears to work. Always test in a fresh session.

What to do when the skill grows

If you add MySQL support, do not double the body. Move dialect detail into references and point to them:

## Dialect notes
- Postgres: the rules table above.
- MySQL: read [references/mysql.md](references/mysql.md) before reviewing.

Claude reads mysql.md only when the migration is MySQL. Keep references one level deep, linked directly from SKILL.md.

For a more complete, production-grade version of this idea, compare your result with the schema-migrations skill, which covers expand/contract deploys, batched backfills, and rollback plans.

Next steps

Frequently asked questions

How long does it take to write a useful skill?

A first working version takes 15 to 30 minutes if you already know the procedure you want to encode. Most of the value comes from the next few iterations, after you watch Claude use the skill on real tasks and tighten the steps it skipped or misread.

Should I write the skill myself or ask Claude to write it?

Both work well together. Do the task once with Claude in a normal conversation, then ask it to turn what worked into a skill. Review the draft yourself and cut anything Claude already knows. The skill-authoring skill on this site scaffolds the folder and validates the result.

What is the best first skill to write?

Pick a procedure you repeat at least weekly that has a right and a wrong way to do it, such as reviewing migrations, writing release notes, or triaging bug reports. Avoid general knowledge Claude already has; a skill earns its place by encoding your team's specific rules.

esc