Introduction
We have been spending time teaching Claude Code how to help us upgrade Rails applications. Along the way we learned that the most reusable piece of that effort is not the prompt we type, it is the skill we wrote once and now load on demand.
If you keep pasting the same checklist or procedure into a chat, a skill is probably what you want. This post walks through writing your first skill from scratch and keeping it lean, so it triggers when you want and costs almost nothing when you don’t.
What Is a Skill?
A skill is a folder with a SKILL.md file inside it. The file has two parts: YAML front matter that tells Claude when to use the skill, and Markdown instructions that tell Claude what to do once it is loaded.
That is the whole idea. Claude reads the descriptions of all available skills at the start of a session, and loads the full body of a skill only when it becomes relevant. You can also invoke a skill yourself by typing /skill-name.
One advantage of skills over a large CLAUDE.md is lower cost, a skill’s body loads only when it is used, so long instructions don’t consume tokens unless they’re needed.
When Should You Write One?
Write a skill when you notice any of these:
- You keep pasting the same instructions, checklist, or multi-step procedure into chat.
- A section of your
CLAUDE.mdhas grown into a procedure rather than a fact. - You have a workflow with side effects (deploy, commit, release) that you want to trigger deliberately.
If it is a one-off, just ask. If it is a pattern you repeat, make it a skill.
Where Skills Live?
Where you put the skill folder decides who can use it:
- Personal skills are available in all your projects, at
~/.claude/skills/<skill_name>/SKILL.md. - Project skills are scoped to the project, at
<project>/.claude/skills/<skill_name>/SKILL.md. - Plugin skills are available wherever the plugin is enabled, at
<plugin>/skills/<skill_name>/SKILL.md.
Personal skills are great for your own habits. Project skills get committed to the repo so the whole team shares them. Plugins are how you bundle and distribute skills to other people, which is a topic of its own.
Build a Demo Skill
Let us build a small, real skill. We will keep it Rails-flavored since that is our world. The skill reviews a migration you just wrote and suggests schema improvements you may have missed, such as a foreign key without an index, a boolean without a default, or a column that should probably be NOT NULL. It is more than a grep; it asks Claude to read each migration and reason about it, which makes it a good showcase for everything that follows.
Create the folder in your personal skills directory:
mkdir -p ~/.claude/skills/migration-review
Then write ~/.claude/skills/migration-review/SKILL.md:
---
description: Reviews Rails migrations for schema best practices and suggests improvements, like missing indexes on foreign keys, missing NOT NULL constraints, missing default values for booleans, and missing limits. Use after writing a migration, when reviewing one, or when the user asks how to improve a migration.
---
## Migration changes
!`git diff HEAD -- db/migrate`
## Instructions
Read each migration in the diff above and suggest improvements:
- A foreign key or `*_id` column added without an index.
- A boolean column without a `default` and `null: false`.
- A column that is likely required but is left nullable.
- A `references`/`belongs_to` without `index:` or `foreign_key:`.
- A string column that should have a sensible `limit`.
For each suggestion, report the file, the line, and the change you would make.
If a migration already looks solid, say so. Do not edit the migrations, just suggest.
That is a complete, working skill. Two things deserve a closer look.
One clarification before we go further: this skill runs inside a Claude Code session, not on a raw rails db:migrate you type in a plain terminal. It fires when Claude loads it automatically (because you said something that matches the description) or when you invoke /migration-review yourself. So “after writing a migration” here means “while you are working in Claude Code”, for example right after you ask it to generate a migration. It suggests improvements; it does not enforce them. If you want a rule that fails the build when a convention is broken, that is a different mechanism (a CI check or a linter) that lives in your repo rather than in a skill.
The Description Is the Most Important Line
The description is the only part of the skill that is always in context. It is what Claude reads to decide whether to load your skill at all. A vague description means the skill never triggers; a sharp one means it triggers exactly when you want.
Write it as what it does plus when to use it, and put the key use case first:
description: Reviews Rails migrations for schema best practices and suggests improvements (...). Use after writing a migration, when reviewing one, or when the user asks how to improve a migration.
Notice the trigger phrases: “after writing a migration”, “how to improve a migration”. Those are the words you would naturally say. That is the point. If the skill is not triggering, the fix is almost always in the description, not the body.
One constraint to keep in mind: the combined description text is capped at 1,536 characters in the skill listing, so lead with the part that matters.
Dynamic Context Injection
The !`git diff HEAD -- db/migrate` line is not something Claude runs. Claude Code runs it before sending the skill content, and replaces the line with the command’s output. So Claude receives your actual migration changes inlined, not an instruction to go fetch them. This grounds the skill in real data instead of guesswork.
Keep the Body Lean
Here is the rule that matters most once a skill grows: once a skill is loaded, its content stays in context for the rest of the session. Every line is a recurring token cost. So state what to do, not how or why. The same conciseness test you apply to CLAUDE.md applies here.
A good target is to keep SKILL.md under 500 lines. When it gets bigger than that, you do not write a longer SKILL.md, you split it.
Progressive Disclosure: References and Patterns
This is the technique that makes skills scale, and it is the one most people miss.
A skill folder can hold more than SKILL.md. You can add reference files, examples, templates, and scripts. The trick is that SKILL.md stays small and acts as a map, pointing to the other files. Those files load only when Claude decides it needs them.
migration-review/
├── SKILL.md # the map: overview + when to load what
├── references/
│ └── schema-conventions.md # the full checklist, loaded only when relevant
├── patterns/
│ └── boolean-with-default.md # a worked pattern for a specific scenario
└── scripts/
└── missing_fk_indexes.rb # executed, never loaded into context
In SKILL.md, you reference these so Claude knows what each one is for and when to reach for it:
## Additional resources
- For the full schema conventions checklist and the reasoning behind each rule,
see [references/schema-conventions.md](references/schema-conventions.md)
- For the canonical way to add a boolean with a default, see
[patterns/boolean-with-default.md](patterns/boolean-with-default.md)
The mental model worth internalizing:
references/holds detailed material for specific scenarios. It costs nothing until that scenario shows up. This is where deep, situational knowledge goes, like the full schema conventions checklist and the reasoning behind each rule.patterns/holds worked examples of how you want a particular thing done, like the canonical way to add a boolean column with a default and aNOT NULLconstraint.scripts/holds code Claude executes rather than reads. A script does the heavy lifting deterministically while Claude orchestrates, like scanningschema.rbfor foreign key columns that are missing an index. Reference scripts with${CLAUDE_SKILL_DIR}so the path resolves no matter where the skill is installed.
So the discipline is: lean orchestrator in SKILL.md, situational depth in references/, repeatable code in scripts/. That keeps the always-on cost tiny while letting the skill carry a lot of knowledge.
A Real Example to Learn From
If you want to see these ideas at a larger scale, OmbuLabs maintains an open-source Rails upgrade skill you can read and run: ombulabs/claude-code_rails-upgrade-skill . It encodes our FastRuby.io upgrade methodology into a skill that produces upgrade reports and app:update previews from your actual codebase.
It is a great reference for what a substantial skill looks like. Treat it as a real-world example rather than a style guide: a skill that grows over time will not always match every best practice in the latest docs, and that is normal. The fundamentals in this post (a sharp description, a lean SKILL.md, dynamic context injection, and progressive disclosure through references/, patterns/, and scripts/) are what you should hold onto as you write your own.
Conclusion
- A skill is a folder with a
SKILL.md: front matter says when, Markdown says what. - The
descriptiondecides whether your skill ever triggers. Make it specific and lead with the key use case. - Use dynamic context injection (
!`command`) to ground the skill in real data instead of guesswork. - Keep
SKILL.mdlean, because its content stays in context. Push situational depth intoreferences/, worked examples intopatterns/, and executable code intoscripts/.
Start small. Write a ten-line skill for something you do every day, watch it trigger on its own, and grow it from there.
Need Help Building Developer Tooling?
We have been investing heavily in making AI tools genuinely useful for Rails work, from upgrade skills to custom workflows. If you would like a hand designing skills, automating your upgrade process, or upgrading your Rails app , we would be happy to talk.