Claude Code skills: how to write one that works
10m read time verified against Claude Code 2.1.270 on September 13, 2026

Claude Code skills: how to write one that works

Most Claude Code skills are a prompt with a fancy filename. A proper skill is a procedure that encodes distrust of the model itself. Here is how to write one, using a code-audit skill as the worked example.

A Claude Code skill is a procedure: a SKILL.md holding the steps, the checks and any helper files a job needs, which the agent loads and follows when that job comes up. The reason to write your own is the part most people skip, and it is what the rest of this post is about.

You read the decision tree, you picked "skill" out of the seven ways to steer Claude Code, and now you have an empty SKILL.md open. What do you actually put in it?

Here is what most people put in it: a prompt. A paragraph or two of "audit this code and tell me what's wrong", saved under a directory with a nice name. It loads, the model reads it, the model probably does roughly what it says. Probably.

That is not a skill. That is a prompt with a fancy filename.

A proper skill is a procedure. And the best ones do something a prompt can never do: they encode distrust of the model that runs them. They name exactly how the agent is going to go wrong, and they build the guardrail into the steps so it can't.

Let me show you what I mean with a real one. Take a code-audit skill: point it at a path or a git range, it hunts for defects and writes a report. It is a good example precisely because the interesting part is not the part you'd expect.

What a Claude Code skill actually is ​

Strip away the mystique first, because it matters that the container is boring.

A skill is a folder with a SKILL.md in it: YAML frontmatter at the top, markdown instructions below. Where you put the folder decides who gets it:

LocationWho gets it
~/.claude/skills/<name>/SKILL.mdyou, in every project
.claude/skills/<name>/SKILL.mdeveryone working in the repository
packages/api/.claude/skills/<name>/SKILL.mdClaude, whenever it works on files in that package
skills/<name>/SKILL.md inside a plugineveryone who installs the plugin

Two things do the real work. The folder name becomes the slash command, so /code-audit runs the skill directly, or /plugin-name:code-audit for a plugin's skill. The description field is what Claude reads to decide whether to load the skill by itself, which is why it should name the situations that call for it. Of the optional fields, two are worth knowing: disable-model-invocation: true leaves the skill to the slash command alone, and allowed-tools limits the tools it may use.

Anything heavier, such as a reference doc or a helper script, lives in a sibling file the skill points to. Claude reads a reference file only when the task needs it, and runs a script without loading it into context. I walked through installing and organising these in the superpowers plugin guide.

The container is deliberately dumb. All the value is in what the body tells the agent to do, and in how tightly it constrains the ways that can go wrong.

So the real question was never "what's the file format". It's "what separates a skill worth loading from a saved prompt". Three things, and code-audit shows all three.

A skill is a procedure, not a prompt ​

Here is the naive version of code-audit, the one most people would write:

Audit this code for bugs, security issues, and bad logic. Report what you find.

Drop that on a model and it will read some files, pattern-match against things that look like bugs, and hand you a confident list. Some of it real. Some of it hallucinated. You have no way to tell which, so you have to re-check all of it by hand, which is the work you were trying to avoid.

The actual code-audit skill is a procedure with three phases. It partitions the target into audit units small enough that one subagent can read every line. It spawns a finder per unit across four fixed dimensions, plus one cross-cutting finder for contradictions that are invisible to any single-unit reader. And then it does the move that separates a skill from a prompt:

No finding is asserted until a fresh, independent skeptic subagent has tried to refute it and failed.

Read that again, because it is the entire point. Every finding a finder produces gets handed to a new subagent whose only job is to kill it. That skeptic receives the claim and nothing else. Not the finder's reasoning, not its confidence, not its justification. Here is the skeptic's actual instruction, trimmed:

text
You are a skeptical senior engineer. A code audit produced this claim. Your job is to REFUTE it.
Claim: in [file:line], [dimension] defect: [failure scenario]. Offending code: [code].
Read the actual file(s) in full, including surrounding code, callers, and any guards
elsewhere that would prevent the failure. Then decide:
- REFUTED: the failure cannot actually happen. Default to REFUTED if you cannot confirm
  it end-to-end.
- CONFIRMED: you traced the failure scenario end-to-end and it happens. Quote the exact
  lines that prove it.

Notice the default. REFUTED unless proven, with the burden on the finding to survive rather than on the skeptic to demolish it. The skeptic reads the actual file in full, including the guards and callers elsewhere that might make the failure impossible, and it never sees the argument that made the finding sound convincing in the first place.

That design starts from an assumption most prompts never make: the model generating findings is not trustworthy. It will produce plausible-looking bugs that dissolve the moment someone actually checks. So the skill builds the check in. It sets the model's output against a fresh instance of the same model, stripped of the reasoning that made the claim feel true, and only the survivors reach you.

This is the difference. A prompt tells the agent what to do. A procedure tells it what to do and how it's going to be wrong while doing it.

The craft is naming how the agent goes wrong ​

Once you accept that a skill is guardrails against the model's own failure modes, the rest of the craft follows. Here's where it shows up.

The description is a trigger, not a summary. This is the one everyone gets backwards. The description field exists so a future agent, scanning a list of skills, can decide whether to load this one. It should describe when to use the skill, not what the skill does. Here is code-audit's real frontmatter:

yaml
---
name: code-audit
description: Use when asked to audit code — "audit this module", "logic audit",
  "fact-checked review", "/code-audit [path|ref]" — for a path, git ref/range, or the
  whole repo. Runs finder subagents across four dimensions, then adversarially verifies
  EVERY finding with independent skeptic subagents before asserting it. NOT for reviewing
  the current working diff (use /code-review) and NOT a fix-applying tool — it is strictly
  read-only.
---

It leads with the phrases someone would actually say and, crucially, spends its back half on when not to reach for it. It is not perfect. That middle clause, "runs finder subagents, then verifies", is exactly the workflow summary I just told you to cut, and you could. But the two ends earn their place: the quoted triggers up front so the skill gets found, and the loud NOTs at the back so it does not fire on the wrong job.

Why does this matter so much? Because if you summarise the workflow in the description, the agent reads the summary and thinks it now knows the procedure, so it never opens the body. A description that says "audits code by finding and verifying issues" quietly teaches the agent to skip the part where it learns how. Keep the process out of the description. The description gets you in the door. The body does the teaching.

The output contract is a recipe, not a wish. A finder in code-audit can't just say "this looks wrong". The skill forces every finding into a fixed shape: dimension, severity, exact file and line, the offending code quoted, and a failure_scenario that has to be a concrete input or state leading to a concrete wrong outcome. The skill spells it out: "This looks wrong" is not a scenario. An empty list is explicitly allowed and called respectable.

That rigidity is the point. "Report anything suspicious" is an open invitation to pad the list with vibes. A required, concrete shape gives the model nothing to negotiate with. Either you can name the input that breaks it, or you have no finding. The same principle runs through why the prompt is not the spec: loose instructions get loose output, and the fix is to make the shape non-optional.

Close the loopholes before the agent finds them. code-audit ends with a table of discipline rules, each marked "no exceptions", and a red-flags list titled "stop and correct yourself". A slice of that table:

RuleNo exceptions
One skeptic per finding for critical/high severityNever batch these
Never override a skeptic's REFUTED back to confirmedIf you disagree, spawn a second skeptic, majority wins
No finding skips verificationMove low-severity items to an Unconfirmed appendix, never assert unverified
Finder dies or returns garbageMark its unit "not audited", never fabricate coverage

Every one of those lines exists because it's a shortcut a model under pressure will reach for. Batching skeptics to save time. Quietly promoting a refuted finding back because it feels right. Papering over a crashed subagent so the report looks complete. Writing these into the skill isn't paranoia, it's memory: it's every way this exact procedure has gone wrong, written down so it can't go wrong the same way twice. A skill without a red-flags section is a skill that hasn't met its own failure modes yet.

Match the effort to the target. code-audit has a scaling rule: if the target is five files or fewer, skip the finder fan-out and do the find pass inline. But the verification never degrades, even a tiny target still gets fresh skeptic subagents. That's judgement encoded directly. Don't spin up eight subagents to audit a helper file, but never let the core guarantee slip just because the job looked small.

What to actually do ​

Next time you're staring at an empty SKILL.md, work in this order.

  • Write the description as a trigger. Start with "Use when", load it with the words someone would actually type or say, and state where the skill does not apply. Do not summarise the steps. If your description explains the workflow, delete that half.
  • Write the procedure, not the intention. If your body reads like something you'd type into the chat box, it's a prompt. Break it into phases with a clear job each. Name what gets read, what gets produced, and in what order.
  • Find the step where the model will lie to you, and build the check in. This is the actual craft. Where does the procedure depend on the model's own output being correct? That's where you add the independent verification, the skeptic, the second pass that can't see the reasoning that made the first pass feel right.
  • Make the output a contract. Fixed fields, concrete evidence required, "an empty answer is a valid answer" stated out loud. Give it nothing to pad with.
  • Write down every way it's gone wrong. A no-exceptions table and a red-flags list. Not hypothetically, from watching it actually fail. If you've never watched the skill fail, you don't yet know what to put there, which means you're not done.

A prompt asks the model to be good. A skill assumes it won't be, and holds the line anyway. That is the whole difference, and it's why the ones worth keeping all read less like instructions and more like a list of the ways their author has already been burned.

(15 of 36)
1My Claude Code setup: status line, plugins and terminal2Superpowers: teaching Claude Code to think before it types3Claude Code hooks: deterministic control over AI workflows4The CLAUDE.md file: give your AI permanent memory5Stop asking your agent nicely6What's new in Claude Code: notes from the London talk7The best number in Opus 4.8 isn't a benchmark8Stale memory is worse than no memory9The agent is just a loop10Build an MCP server, then ask whether it should exist11Skill, subagent, hook, or slash command? Pick the right one12Log in to MCP servers from your shell13How to give Claude safe access to your SQL database14The day 'default' became 'Manual'15Claude Code skills: how to write one that works16How to write a proper Claude Code subagent17Claude Code permissions: the guide I wish the docs were18Sandboxing Claude Code: put your agent in a box that holds19Prompt injection defense for developers who ship agents20Best Claude model for coding: which one for which task21Refactoring legacy code with a coding agent: start with characterization tests22MCP server authentication: OAuth, scopes and rate limits23Opus 5 is here and your effort settings just expired24AI agent incident response: what to do when your coding agent goes wrong25Claude Code /doctor: what it checks and what changed26Claude Code context management: when to /clear and when to /compact27Git worktrees for parallel coding agents: what they isolate and what they share28Claude Code plan mode: decide before the agent writes29Debugging with a coding agent: give it the search, keep the hypothesis30Claude Code checkpoints and /rewind: how to undo an agent's changes31Claude Code cross-session messaging: how to make your sessions talk to each other32Audit logging for AI agents: what Claude Code records and what deserves a human33Sharing Claude Code config across a team: what a repo can and cannot enforce34Your Claude Code session has no clock35Claude Code in a large codebase: scoping an agent to the part that matters36Recovering a Claude Code session your picker will not show you