Every team has a chore it explains again and again. For a .NET team with EF Core it is the migration: change the model, create the migration with the tool, read it for data loss, run the tests, and never apply it to a database on your own. A Claude Code skill is that procedure written down once. This part writes one, runs it in seven real sessions with and without the file, and reports both what it changed and what it did not.

Steps in this part
  1. Save SKILL.md.txt as .claude/skills/add-migration/SKILL.md in your repository and change the paths and names to yours
  2. Allow the commands it runs with settings.json: build, test and dotnet ef migrations for both Bash and PowerShell, and a deny on dotnet ef database
  3. Ask for a model change in plain words. You should see the first tool call be Skill with Launching skill: add-migration
  4. Or call it by name: /add-migration Add an optional phone number to patients.
  5. Read the summary it ends with: what Up does, what Down does, any data loss, the test result
  6. Check git status: the entity, the context, the snapshot and two new migration files, and no database touched

What a skill is

A skill is a folder with a SKILL.md file: a few lines of frontmatter, then instructions in Markdown. A project skill lives in .claude/skills/<name>/SKILL.md and is committed with the code, so the whole team gets it. Claude Code keeps only the name and description of each skill in the conversation and loads the body when the skill is used, either because the description matches what you asked for or because you typed /name. The skills documentation draws the line with CLAUDE.md this way: facts that hold in every session go in CLAUDE.md, and a multi-step procedure goes in a skill.

That split has a measurable price. In these runs, having the skill in the repository added about 100 tokens to every turn (20,967 against 20,870 for the same prompt), and its body cost about 1,100 tokens once, when it was used.

The skill

The frontmatter is three fields. The description is what Claude matches your request against, so it says when to use the skill, not only what it does:

---
name: add-migration
description: Changes the AppointmentDesk EF Core model, creates the matching migration with dotnet ef, reviews it for data loss and runs the tests. Use when asked to add, remove, rename or change an entity property, column, table, relationship or index, or to create a migration.
argument-hint: "[model change]"
---

The body is nine numbered steps. Three of them carry the knowledge a newcomer to this repository would not have: the exact command, what data loss looks like in a generated migration, and the one thing never to run.

3. **Create the migration** with the tool and a PascalCase name that says what changed:
   `dotnet ef migrations add <Name> --project src/AppointmentDesk.Api -o Data/Migrations`
4. **Review the generated `<timestamp>_<Name>.cs`, Up and Down, before anything else.** Data loss looks like:
   - `DropColumn` or `DropTable`;
   - `AlterColumn` that narrows a type or a length, or makes a column NOT NULL;
   - `DropColumn` plus `AddColumn` on one table when the user asked for a rename;
   - the tool printing "An operation was scaffolded that may result in the loss of data".
8. **Never run `dotnet ef database update`**, or any other `dotnet ef database` command. Applying a migration is a person's decision.

Step 2 says to build before creating the migration, step 5 says a rename must be a rename and how to get one, and step 7 says why the tests matter here: this solution's integration tests apply every migration at startup, so a model change without its migration fails them. Write the equivalent facts for your own repository; they are the part that pays.

The permissions it needs

The settings file allows editing under src and tests, and dotnet build, dotnet test and dotnet ef migrations add, list and remove, each once for the Bash tool and once for the PowerShell tool. On Windows every dotnet command in these sessions went through PowerShell, so a Bash-only rule would have allowed nothing. It denies dotnet ef database * for both.

One thing to know before you test this from a script. In a folder Claude Code has never been opened in interactively, claude -p ignores the allow rules of the project file and says so:

Ignoring 12 permissions.allow entries from .claude/settings.json: this workspace has not been trusted. Run Claude Code interactively here once and accept the trust dialog

These sessions passed the same twelve rules with --allowedTools instead. Part 2 covers trust and the rule syntax in full.

A new column: the skill changed nothing in the code

The prompt was Add an optional phone number to patients. With the skill present, the session's first move was to load it:

TOOL CALL Skill: {
 "skill": "add-migration",
 "args": "Add an optional phone number to patients."
}

TOOL RESULT (Skill):
Launching skill: add-migration

It added PhoneNumber with a length of 30, built, ran the exact command from step 3, read the generated file, ran the tests, and ended with a summary that included this line: "I haven't applied the migration to any database (dotnet ef database update); that's your call."

Then the same prompt on a copy without the skill. The result was the same code: the same property name, the same length, the same migration name, a diff identical apart from the timestamp. A current model knows how to add a column. The only difference was a detour: without the skill, the session ran dotnet ef on the fresh copy before any build and hit error NETSDK1004 (no restore yet), then built and tried again. Step 2 of the skill avoids that.

A rename: the surprise was EF Core

The plan was to show the classic trap. Microsoft's migrations guide warns that a renamed property is scaffolded as a dropped column plus a new one, which loses the data. On EF Core 10.0.12, Rename Patient.FullName to Name. did not do that. With or without the skill, by hand or in a session, the migration came out as a rename, with no warning:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.RenameColumn(
        name: "FullName",
        table: "Patients",
        newName: "Name");
}

So the skill's review step had nothing to catch. The session without the skill is the instructive one, for a different reason. It first tried dotnet ef --version, which was not in the allow list and was denied. Instead of running the migration command, which was allowed, it said "I can't run dotnet ef without approval, so I'll write the migration by hand", and wrote the migration, its Designer file and the snapshot edit itself, with a made-up timestamp of 12:00:00. The content matched what the tool produces. It is still three generated files written by hand, in a repository whose rule is that nobody does that.

A rename with a second change: where the data loss is

The drop and add appeared when the rename came with another change to the same column: Rename Patient.FullName to Name and allow names up to 150 characters. Without the skill, one migration was scaffolded, and EF printed its warning:

An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
migrationBuilder.DropColumn(
    name: "FullName",
    table: "Patients");

migrationBuilder.AddColumn<string>(
    name: "Name",
    table: "Patients",
    type: "TEXT",
    maxLength: 150,
    nullable: false,
    defaultValue: "");

Applied as written, that empties every patient's name. The model caught it without any skill, from the warning: "EF scaffolded it as drop FullName + add Name, which would wipe every existing patient's name in the database." It rewrote the file by hand as a RenameColumn followed by an AlterColumn, which keeps the data and, on SQLite, rebuilds the whole table. Its summary then claimed the tests "never run migrations", which is false here: five of the fourteen do.

With the skill, the session never produced the dangerous migration at all. Step 5 tells it to make a rename on its own first, and it did: "Doing the rename on its own first, then the widening as a second migration." Two migrations, both made by the tool, the first a plain RenameColumn, no hand edit and no table rebuild. Its summary got the tests right.

Calling it by name

A headless run accepts the slash form: claude -p "/add-migration Add an optional phone number to patients." followed the same steps in fewer turns (11 against 14) and produced the same code. It leaves no trace in the transcript, though: there is no Skill tool call, because Claude Code expands the skill into the prompt before the session starts. A probe that asked the model to quote the first heading and step it had been given returned the skill's own text word for word.

What it cost

Seven sessions on fresh copies, Claude Code 2.1.287, Opus 5.5; cost is the notional figure the tool reports
RequestSkillTurnsTimeCostHow the migration was made
Add a phone numberyes1455 s$0.19the tool
Add a phone numberno1649 s$0.22the tool, after a failed first try
Renameyes2153 s$0.27the tool
Renameno2268 s$0.39three files written by hand
Rename and widenyes2674 s$0.36the tool, two migrations
Rename and widenno2983 s$0.43the tool, then rewritten by hand
Add a phone number, by nameyes1140 s$0.17the tool

After every run the solution built and all fourteen tests passed, and no session in either group ran a dotnet ef database command.

What it did not do

The honest summary is that the skill did not make the model smarter. Without it, the model added the column correctly and caught the data loss on its own. What the skill changed was the route: the exact command every time, no hand-written migration files, the risky change split before the tool could generate a risky migration. That is one run per case, so the hand-written migration and the false statement might not repeat on a second try.

Two more limits showed. The step that says build, then test, was only half followed: after creating the migration, every skill run went straight to dotnet test. And every skill run first tried to chain the build and the migration in one PowerShell line with an if, which was refused even though both commands were allowed:

dotnet appears inside a control-flow or chain statement where piped expression sources cannot be statically validated and requires manual approval

Each session recovered by running the commands one at a time. A skill is guidance, and guidance gets skipped. A rule that must hold every time, such as nobody edits a file under Data/Migrations by hand, belongs in a hook, which is Part 3. The deny rule on dotnet ef database was never tested here either, because no session tried.

Frequently asked

Where do Claude Code skills go in a .NET repository?
In .claude/skills/<name>/SKILL.md at the repository root, committed with the code. The folder name becomes the command, so .claude/skills/add-migration/SKILL.md gives you /add-migration, and the description in its frontmatter tells Claude when to load it by itself.
What is the difference between a skill and CLAUDE.md?
CLAUDE.md is read in every session, so it holds short facts that always apply: build commands, conventions, layout. A skill holds a multi-step procedure and its body is loaded only when used. In these runs the skill's listing cost about 100 tokens per turn and its body about 1,100 tokens once.
Does EF Core 10 turn a property rename into a drop and add?
Not for a plain rename in this test: on EF Core 10.0.12 with SQLite, renaming Patient.FullName to Name scaffolded a RenameColumn with no warning. A rename combined with a length change on the same column scaffolded DropColumn plus AddColumn and printed the data-loss warning, so review every generated migration.

Next: Part 5, a reviewer subagent, and what a second reader finds that the first one missed.