Writing Skills for Claude Code
18. April, 2026 • 15 min read • Develop
Explaining the same thing every morning
You start a session, and before anything useful happens you type the same paragraph you typed yesterday. Our commits look like this. The migrations live over there. Run the linter before you push, and no, not that linter. It is the software equivalent of repeating your coffee order to someone who has been making it for a year.
Claude Code has a mechanism for this, and it’s about as low-tech as it could be: a folder with a markdown file in it. Write the instructions down once, and they get loaded when they’re relevant. That’s a skill.
I’ve written about AI in the editor before, back when the argument was still whether autocomplete counted as cheating. That argument is over. The interesting question now is how much of your team’s tacit knowledge you can hand over, and skills are the sharpest tool I’ve found for it.
What the thing is, briefly
Claude Code is Anthropic’s agentic coding tool. You start it in a project directory:
claudeand then talk to it. The word doing the work is agentic: it doesn’t propose a diff for you to apply, it reads files, edits them, runs the tests, and comes back with what happened. One request routinely becomes thirty tool calls.
What it can do, roughly:
- Read and navigate the whole project, including how the pieces connect
- Edit across as many files as a change touches, so renaming a component updates the imports
- Run builds, tests, linters, and anything else in your shell
- Stage, commit, branch, and open pull requests
- Spawn parallel subagents for work that splits cleanly, with a lead agent coordinating
- Run in the terminal, in VS Code and JetBrains, in a desktop app, and in the browser
It also reads CLAUDE.md from your project at the start of every session. That file is the standing brief: architecture, conventions, the commands that matter. Skills are the opposite shape, loaded only when needed.
A skill is a folder with a markdown file in it
Minimum viable skill: a directory containing SKILL.md, which is YAML frontmatter followed by instructions in plain English.
The mechanism that makes this scale is what the spec calls progressive disclosure. At startup Claude reads only each skill’s name and description. The body stays on disk until the skill is actually used. So thirty skills cost you thirty short descriptions of context, not thirty documents, and there’s no reason to be precious about how many you install.
Two ways in. You type /skill-name and it loads. Or Claude reads the description, decides it’s relevant to what you just asked for, and loads it itself. Both routes matter, and as we’ll see, sometimes you want to close one of them.
Where they live
| Location | Path | Applies to |
|---|---|---|
| Personal | ~/.claude/skills/<name>/SKILL.md |
All your projects |
| Project | .claude/skills/<name>/SKILL.md |
This project only |
| Plugin | <plugin>/skills/<name>/SKILL.md |
Wherever the plugin is enabled |
Personal skills are for habits that follow you around: how you like commits written, how you want code explained back to you. Project skills go in version control and encode things the team agreed on, which means a new colleague inherits them by cloning the repository rather than by asking someone.
When the same name exists at more than one level, personal wins over project, and either wins over a skill that ships with Claude Code. A code-review skill in your project directory replaces the built-in one. Plugin skills are namespaced as plugin-name:skill-name and can’t collide.
In a monorepo, skills also load from nested .claude/skills/ directories underneath your working directory, so a package can carry its own. If the names clash, both stay available and the nested one gets a qualified name like apps/web:deploy. Handy, and also the sort of thing you discover by accident when the wrong deploy skill fires.
The frontmatter
Here’s a skill that does something concrete:
---
name: review-component
description: Reviews React components for accessibility, performance,
and adherence to project conventions. Use when reviewing PRs or
checking component quality.
---
When reviewing a React component, check the following areas:
## Accessibility
- All interactive elements have appropriate ARIA attributes
- Images have alt text
- Colour is not the only means of conveying information
- Keyboard navigation works correctly
## Performance
- No unnecessary re-renders (check dependency arrays)
- Large lists use virtualisation
- Images are optimised and lazy-loaded
## Conventions
- Component uses TypeScript with proper prop types
- Follows the project's naming conventions
- Uses Tailwind utility classes, not inline styles
- Tests cover the main interaction paths
Provide specific line references for any issues found.Two fields is often all you need. The rest are there for control:
---
name: deploy-production
description: Deploys the application to production. Runs tests,
builds the Docker image, and pushes to the container registry.
disable-model-invocation: true
allowed-tools: Read, Grep, Glob, Bash(npm run *)
argument-hint: "[environment]"
model: inherit
---description is the field that decides whether a skill ever gets used. Claude only sees this line when choosing, so it has to say both what the skill does and when to reach for it. “Helps with components” is useless. Keep it tight, too: long descriptions get truncated in the listing, and the truncated half is the part you cared about.
name is less important than it looks. For a personal or project skill it’s a display label; the command you type comes from the directory name. ~/.claude/skills/deploy-staging/SKILL.md gives you /deploy-staging whatever the frontmatter says. Only in plugin skills does name set the final segment of the command.
disable-model-invocation: true stops Claude from loading the skill on its own. Anything with side effects wants this, and I’d argue for it more broadly than most people do. The failure mode isn’t dramatic, it’s just that a skill you wrote for occasional use fires during unrelated work because the description happened to match.
allowed-tools pre-approves tools for the turn that invoked the skill, so you’re not clicking through permission prompts mid-workflow. The grant expires when you send your next message. You can scope it narrowly, as Bash(gh *) rather than all of Bash.
argument-hint shows up in autocomplete. model accepts the same values as /model, plus inherit to leave the session model alone. Reaching for a cheaper model on a mechanical skill is a reasonable instinct, though I’d measure before assuming it saves anything.
Who can invoke what
| Configuration | You | Claude |
|---|---|---|
| Default | Yes | Yes |
disable-model-invocation: true |
Yes | No |
user-invocable: false |
No | Yes |
The third row is for background knowledge that shouldn’t clutter the / menu: house API conventions, a naming policy, something Claude should know when it’s relevant and you’d never type by hand.
Arguments
Whatever follows the skill name lands in $ARGUMENTS:
---
name: fix-issue
description: Reads a GitHub issue and implements the fix
argument-hint: "[issue-number]"
---
## Issue context
Read GitHub issue #$ARGUMENTS and understand the problem.
## Implementation
1. Find the relevant code
2. Implement the fix
3. Write tests
4. Create a commit with message: "fix: resolve #$ARGUMENTS"/fix-issue 42 substitutes 42 everywhere the placeholder appears. If the skill doesn’t contain $ARGUMENTS at all, what you typed gets appended at the end anyway, so nothing is silently dropped.
For several arguments there’s positional access, $ARGUMENTS[0] or the shorter $0, $1 and so on. Multi-word values need quoting the way a shell would expect: /migrate-component "Search Bar" React Vue.
Injecting live context
This is the feature that changed how I write skills. The !`command` syntax runs a shell command before the skill is handed to Claude, and the output takes the placeholder’s place:
---
name: pr-summary
description: Summarises the current pull request
context: fork
agent: Explore
allowed-tools: Bash(gh *)
---
## Pull request context
- Changed files: !`gh pr diff --name-only`
- PR description: !`gh pr view --json body -q .body`
- Review comments: !`gh pr view --comments`
## Task
Summarise this pull request. Focus on:
1. What changed and why
2. Any concerns raised in review comments
3. Suggested improvementsWorth being precise about what’s happening, because it’s easy to misread. This is preprocessing. The commands run first, their output is pasted in, and Claude receives a finished prompt containing the actual diff. It never sees the commands, and it isn’t deciding to run them.
Some sharp edges. Substitution happens once over the file, so output containing another placeholder won’t be expanded again. The inline form only triggers when ! starts a line or follows whitespace, which means KEY=!`cmd` is left as literal text and quietly does nothing. For several commands, open a fenced block with a ! right after the backticks instead of stringing inline placeholders together.
There’s also a disableSkillShellExecution setting that turns the whole feature off, replacing each command with a note that policy blocked it. If you’re introducing skills somewhere with a security team, know that this exists before they ask.
Running a skill in a subagent
Research burns context. A skill that reads forty files to answer one question will leave your main conversation full of file contents you’ll never refer to again. context: fork runs it somewhere else:
---
name: deep-research
description: Performs thorough codebase research on a topic
context: fork
agent: Explore
---
Research the following topic in this codebase: $ARGUMENTS
Trace through all relevant code paths. Map the architecture.
Identify patterns, abstractions, and dependencies.
Return a summary with the 10 most important files
to understand this area of the codebase.The skill body becomes the subagent’s prompt. It gets the codebase and the tools, but not your conversation history, and only its answer comes back.
That last point has a consequence worth knowing: the built-in Explore and Plan agents deliberately skip CLAUDE.md to keep their context small. So a forked skill using agent: Explore sees your skill content and nothing else about the project. If it needs a convention to do its job, the convention has to be written into the skill.
The other trap is forking a skill that has no task in it. If your skill is a page of guidelines with no instruction, the subagent receives the guidelines, has nothing to do, and returns nothing useful. context: fork is for skills that ask for something.
More than one file
A skill doesn’t have to be a single document. It can be a directory of supporting material:
review-security/
├── SKILL.md # Main instructions
├── owasp-checklist.md # Reference material
├── examples/
│ ├── good-review.md
│ └── bad-review.md
└── scripts/
└── scan.shSKILL.md stays short and points at the rest, which gets read only if it’s needed. Use ${CLAUDE_SKILL_DIR} when referring to bundled files so the paths don’t depend on where the session started.
Templates work well for anything where the shape of the output matters more than the wording:
<!-- template.md -->
## Security review: {{component_name}}
### Summary
{{one_paragraph_summary}}
### Findings
{{findings_table}}
### Risk level
{{low|medium|high|critical}}
### Recommended actions
{{numbered_action_items}}Then, in SKILL.md, a line telling it to fill in template.md and leave nothing blank. A filled template is far more reliable than a paragraph asking for consistent formatting.
The ones that ship with it
Claude Code comes with bundled skills you can read for reference. /code-review and /batch and /loop are the ones I’ve opened most. /batch in particular is a decent lesson in decomposition: it splits a large change into independent units, runs them in separate git worktrees, and opens a pull request per unit. /loop re-runs a prompt on an interval, which is a surprisingly good way to babysit a deploy.
Three that earn their keep
Commits
---
name: commit
description: Creates a conventional commit from staged changes.
Analyses the diff, determines the commit type, and writes a
descriptive message.
disable-model-invocation: true
---
Create a git commit following the Conventional Commits specification.
## Steps
1. Run `git diff --staged` to see what's being committed
2. Analyse the changes to determine the type:
- `feat`: New feature
- `fix`: Bug fix
- `refactor`: Code restructuring
- `docs`: Documentation only
- `test`: Adding or updating tests
- `chore`: Maintenance tasks
3. Write a commit message: `type(scope): description`
- Scope is the primary area affected (component name, module)
- Description is imperative mood, lowercase, no full stop
- Mark a breaking change with `!` after the type, as in `feat!:`,
or with a `BREAKING CHANGE:` footer
- Add a body if the "why" isn't obvious from the diff
4. Include `Co-Authored-By: Claude <noreply@anthropic.com>`
5. Show the message and ask for confirmation before committingThis is conventional commits written down where something else can follow them. Note step 5: skills that touch git should show their work before doing it.
Scaffolding a component
---
name: create-component
description: Generates a new React component with TypeScript,
tests, and a Storybook story following project conventions.
argument-hint: "[ComponentName]"
---
Generate a new React component named `$ARGUMENTS`.
## Files
- `src/components/$ARGUMENTS/$ARGUMENTS.tsx`
- `src/components/$ARGUMENTS/$ARGUMENTS.test.tsx`
- `src/components/$ARGUMENTS/$ARGUMENTS.stories.tsx`
- `src/components/$ARGUMENTS/index.ts` (barrel export)
## Component conventions
- TypeScript with an exported Props interface
- `forwardRef` for components that render DOM elements
- Tailwind for styling, no CSS modules
- JSDoc comment on the Props interface
- Named export, not default
## Story conventions
- CSF3 with `satisfies Meta<typeof Component>`
- Include `tags: ['autodocs']`
- Stories for Default, WithProps, EdgeCaseEvery project has this shape written down somewhere nobody reads. Here it produces files. The story conventions line up with what I covered in the Storybook post.
A read-only explorer
---
name: explore
description: Read-only codebase exploration. Use when you want to
understand code without any risk of modification.
allowed-tools: Read, Grep, Glob
context: fork
agent: Explore
---
Explore this codebase to answer: $ARGUMENTS
You are in read-only mode. Search files, read code, and trace
execution paths. Do not suggest changes. Focus entirely on
understanding and explaining what exists.
Return:
1. A clear explanation of what you found
2. Key files involved, with paths and line numbers
3. An ASCII architecture diagram if it helpsThe one I use most, and the one I’d write first on an unfamiliar codebase.
CLAUDE.md or a skill?
The split is simple enough. CLAUDE.md is loaded every session and should contain facts that are always true: what this project is, how it’s laid out, which commands to run. A skill is loaded on demand and should contain a procedure.
The test I use: if it reads as a paragraph of description, it belongs in CLAUDE.md. If it reads as numbered steps, it’s a skill. When a section of CLAUDE.md grows a numbered list, that’s usually the moment to move it out. /init will generate a first CLAUDE.md from your codebase, which is a reasonable starting point and a poor finishing one.
project/
├── CLAUDE.md
└── .claude/
└── skills/
├── commit/SKILL.md
├── review-pr/SKILL.md
└── create-component/SKILL.mdWhat makes a skill work
One job per skill. A deploy-and-notify-and-update-docs skill is three skills that would compose better separately, and it’s harder to tell which third of it went wrong.
Keep SKILL.md short, a few hundred lines at most, and push detail into files it can reference. Everything in the skill is context that isn’t being spent on your actual code.
Write the description for the moment of selection, not for a catalogue. It should answer “when would I want this?”, because that’s the question being asked of it.
Test with more than one model. A skill that a larger model infers its way through can leave a smaller one guessing, and the fix is usually explicitness rather than length.
Portability, with an asterisk
Skills follow the Agent Skills spec, which Anthropic released as an open standard and which a long list of tools have since adopted, Cursor, Gemini CLI, Codex and GitHub Copilot among them. That’s genuinely useful. A folder with a SKILL.md in it travels.
The asterisk is that the spec is narrower than Claude Code’s implementation. It defines six fields: name, description, license, compatibility, metadata and allowed-tools. Everything else in this post, argument-hint, context: fork, disable-model-invocation, is Claude Code’s own. Packaging a skill for the Skills API or uploading it to claude.ai with those fields present fails outright rather than ignoring them:
Unexpected key(s) in SKILL.md frontmatter: argument-hint.The body features don’t travel either. !`command` injection is Claude Code preprocessing, and elsewhere it’s just text. So “write once, run anywhere” holds for the plain instructional skills and not for the clever ones. Worth knowing before you build a library on the assumption.
The part that gets left out
A skill is a prompt, not a program. Nothing enforces it. Claude reads your numbered steps and generally follows them, and occasionally it decides step three doesn’t apply today and it is, annoyingly, sometimes right about that. If a step must happen every time without exception, it belongs in a shell script that the skill calls, or in a git hook, not in prose asking nicely.
That’s not a complaint so much as the thing to calibrate against. Skills are good at capturing judgement, the stuff that’s hard to encode: which cases matter in a review, what a decent commit message looks like here, where the bodies are buried in this module. They’re bad at guarantees. Anything you’d write a test for, write a test for.
I’m still poking at the edges of the forked-subagent behaviour, mostly trying to work out how much project context a skill has to restate when agent: Explore skips CLAUDE.md. Not a clean answer yet, so I’ll leave it there.
‘Till next time!