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.
- Save SKILL.md.txt as
.claude/skills/add-migration/SKILL.mdin your repository and change the paths and names to yours - Allow the commands it runs with settings.json: build, test and
dotnet ef migrationsfor both Bash and PowerShell, and a deny ondotnet ef database - Ask for a model change in plain words. You should see the first tool call be
SkillwithLaunching skill: add-migration - Or call it by name:
/add-migration Add an optional phone number to patients. - Read the summary it ends with: what Up does, what Down does, any data loss, the test result
- 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
| Request | Skill | Turns | Time | Cost | How the migration was made |
|---|---|---|---|---|---|
| Add a phone number | yes | 14 | 55 s | $0.19 | the tool |
| Add a phone number | no | 16 | 49 s | $0.22 | the tool, after a failed first try |
| Rename | yes | 21 | 53 s | $0.27 | the tool |
| Rename | no | 22 | 68 s | $0.39 | three files written by hand |
| Rename and widen | yes | 26 | 74 s | $0.36 | the tool, two migrations |
| Rename and widen | no | 29 | 83 s | $0.43 | the tool, then rewritten by hand |
| Add a phone number, by name | yes | 11 | 40 s | $0.17 | the 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.