This article won't go over the difference between Skills and prompts, nor explain what MCP/A2A is — there are already plenty of articles covering those.
I'll assume you already know what a Skill is and have written a few yourself.
Below, we're looking at just one thing: how to write Skills that are correct, stable, and maintainable long-term within a team.
Preface: First Distinguish Which Kind of Problem You're Facing
Most people who have written Skills have gotten stuck on one of these two situations:
-
Installed but never used: the Skill is clearly in the directory, yet the agent never touches it once.
-
Used but useless: it triggers occasionally, but as it runs it starts improvising, and the SOP becomes a dead letter.
Faced with these, most people either make the prompt longer or harsher, or pile on a bunch of "MUST", "NEVER", "ALWAYS", "严禁".
But these two situations are completely different things. The former is a routing failure — it happens before the model decides whether to load the Skill at all. The latter is an execution failure — the Skill has been loaded, but the steps, tools, or acceptance criteria weren't followed.
One is "can't find it"; the other is "didn't follow it". The causes are different, so the fixes are different. Mixing them together and fixing both at once is why so many Skills never get better.
Let's talk about how to handle each one properly.
1. A Skill Has Three Layers — It's Not Just a Single File
Many people's Skill is just one lump of SKILL.md — trigger conditions, steps, reference material, examples, caveats, all crammed into a single file.
Such a Skill works in the short term but quickly goes out of control: the body grows longer and longer, and the critical constraints get diluted.
A better approach is progressive loading, which splits discovery, comprehension, and execution into three layers with completely different costs.
| Layer | When it's loaded | What it carries | Design responsibility |
|---|---|---|---|
| L1 Metadata | Always loaded at startup | name, description | Precise discovery & routing |
| L2 Instructions | Loaded only after relevance is judged | SKILL.md body: steps, constraints, examples | Controlling execution behavior |
| L3 Resources & code | Loaded on demand | Scripts, templates, reference material | Providing facts, determinism, and artifacts |
These three layers are officially endorsed by Anthropic. The benefit: in the resident context, a single Skill costs only a few tokens of metadata; most of the cost is paid only after loading.
Wherever you cut corners when writing a Skill, that's where problems accumulate:
-
Stuffing body text into L1 bloats the metadata, and every task pays tokens for Skills that were never used.
-
Stuffing reference material into L2 means every trigger reads a pile of irrelevant info, and the key information gets drowned out.
-
Describing deterministic logic in natural language in L2 makes the model "roughly follow along", and boundary conditions start to fail.
Below, I'll walk through each layer and how to do it better.
2. L1: Triggering
The description is what routing and discovery run on
2.1 The vast majority of "not working" cases come down to this one line
The description is the single line in the whole Skill most worth polishing. It decides whether the model will select this Skill at all.
The most common anti-pattern looks like this:
❌ Anti-example
name: docs-helper
description: Helps handle documents and related content.
The problem with this line: it only says "roughly what I can do" — it doesn't say "under what circumstances to come find me", nor "under what circumstances NOT to come find me". The model can only guess with a description like this.
A good description should answer at least three things:
-
What it does (capability statement — describe the business outcome in one sentence)
-
When to use it (positive trigger — ideally including the user's real wording)
-
When NOT to use it (exclusion clause + where to go instead)
✅ Good example
name: docx-contract-review
description: |
Reviews contract files in .docx format; checks that required clauses,
party names, dates, and signature/seal fields are complete.
USE WHEN: the input is a .docx contract, or the user says
"help me review this contract" / "see if there's a problem with this agreement".
DO NOT USE FOR:
\- PDF content extraction → use pdf-extract instead
\- Contract revision/redlining → use docx-redline instead
\- General writing advice → not applicable to this Skill
2.2 Exclusion clauses matter more than trigger clauses
"Don't trigger when it's NOT X" improves routing accuracy more than "trigger when it's X". Positive descriptions are inherently fuzzy — "review this contract" and "summarize this contract" are semantically very close, and the model misjudges easily. But if you explicitly write "not applicable to: general summarization, use doc-summarize instead", the boundary becomes clear.
Exclusion clauses have another benefit: they tell the model where to go when routing was wrong. Writing "use pdf-extract instead" effectively hands the model a routing table — far more useful than merely saying "don't use for PDF".
2.3 The trigger description should contain the user's actual words
The phrases listed under "USE WHEN" are not for hard keyword matching; they compensate for the model's inference bias about how people really speak.
Users don't say "please execute the canary release coordination process". They say:
-
"Roll payments-api out to prod"
-
"Let 5% of traffic through first"
-
"Validate version 1.42.0 with a small traffic slice first"
Write these real phrasings in, and routing hit rate improves noticeably.
2.4 An easy physical pitfall: confirm it actually "exists" first
Before rewriting the description, first confirm the Skill is really being scanned.
I once hit this: a Skill installed with npx skills add actually landed in ~/.agents/skills/, but the Claude Code I was using at the time scanned ~/.claude/skills/. Install location and runtime scan location are two different things. The user thinks it's installed, but the environment doesn't have it at all. The exact directory names vary by toolchain and version — don't memorize those two paths; the key is realizing that "where it's installed" and "where the runtime scans" can be two different places.
So the debugging order should go from the outside in:
Physical existence → manifest declaration → routing selection → body loading → script execution → output acceptance
If the directory isn't even scanned, rewriting the wording a hundred times won't produce a single signal.
2.5 Control how many Skills are mounted at once
Mounting too many Skills makes them compete for attention, and the false-trigger rate rises noticeably. In practice, keeping the simultaneously mounted count under 20 is safer. Package them by role or scenario; don't dump everything in at once.
If you genuinely need many, layer them: keep a small set of high-frequency ones resident, and load the rest dynamically per project or workspace.
3. L2: The Body
SKILL.md is a map, not a warehouse
3.1 Default assumption: "the model is already smart"
The most common mistake in writing Skills is treating the model like an elementary school student and teaching it everything.
Actually, you only need to write what it doesn't know. Domain conventions, internal company processes, and non-obvious pitfalls — that's what the Skill body should carry. Fluff like "read the code carefully first" or "make sure there are no syntax errors" isn't just useless; it dilutes the constraints that actually matter.
3.2 The minimum effective structure of the body
The skeleton I commonly use is this:
Canary Release Coordination
# Goals & Non-Goals
Coordinate a staged production release.
Non-goals: not responsible for cluster provisioning, not responsible for database migration.
# Prerequisites
\- service, image\_tag, and namespace parameters are confirmed;
\- the caller has approved this production change;
\- baseline SLO data already exists.
# Execution Steps
1\. Preflight: run ./scripts/preflight.sh; any non-zero exit code stops immediately.
2\. Plan: output the target version, canary ratio, and rollback threshold.
3\. Execute: strictly run the declared deployment command exactly once.
4\. Verify: run smoke tests; all assertions in tests/smoke.yaml must pass.
5\. Decide: scale up only if SLO is met; otherwise roll back and emit an audit event.
# Mandatory Checks
\- Deployment operations must be idempotent;
\- canary traffic ratio must stay within policy-allowed bounds;
\- no secrets may be echoed into output.
# Failure & Fallback
\- Only network-class transient failures may be retried once;
\- on threshold-alarm triggers, call rollback.sh and stop immediately;
\- when in doubt, ask exactly one focused question; don't guess at the target service.
# Examples
Input: "Canary payments-api 1.42.0 to prod-us"
→ preflight → release 5% → smoke → scale up or roll back
Counter-example:
Input: "Why is payments-api so slow?"
→ Do NOT invoke this Skill; use k8s-troubleshoot instead.
A few key points:
-
"Non-goals" and "prerequisites" are not optional. The former stops the model from over-expanding its responsibilities; the latter lets the model know to stop and ask when conditions aren't met instead of pushing ahead stubbornly.
-
Steps must be numbered and have stop conditions. Writing "run the test, look at the result, then decide whether to continue" is the same as writing nothing.
-
Examples and counter-examples come in pairs. The counter-example delineates the boundary — prioritize the most easily confused neighboring Skill.
3.3 Control the size
Rule of thumb: keep the SKILL.md body under 500 lines. Beyond that, split it up:
-
Long reference material goes into
references/, with the body stating when to read it; -
Deterministic computation and validation go into
scripts/; -
Output templates and fixed formats go into
assets/.
Anthropic's official guidance: scripts are executed by the agent (bash, python, node, etc. all work), and only the script's output enters the context; reference material is explicitly pointed to by the Skill and only read when needed.
3.4 Don't treat ALWAYS/NEVER as a cure-all
Piling on strong imperative words is a very common misconception, and it has two problems:
-
Overuse breeds fatigue — the model won't be more obedient just because you wrote three MUSTs;
-
It only says "what not to do", not "why" — and when the model hits a boundary case, it's precisely the "why" it relies on for analogical reasoning.
The better style is rule + reason:
❌ Anti-example
NEVER commit code directly to the main branch.
✅ Good example
Don't commit directly to main: every Thursday a release branch is
automatically cut from main, and code committed directly can silently
miss that release train. If you need an urgent fix, open a hotfix
branch and notify the release owner.
Give the reason, and the model can extrapolate correctly when it hits a situation you didn't anticipate.
4. How Strict Should Instructions Be? Set the Level by Risk
My practical experience: how strict an instruction should be is decided by the cost of getting it wrong, not by your level of anxiety.
Lock down every step and the Skill becomes brittle — it stalls the moment the environment shifts even slightly. Leave everything open and high-risk operations become accident-prone.
| Risk level | Typical scenarios | Recommended style | Verification method |
|---|---|---|---|
| High | DB migration, deletion, production writes, permission changes | Exact script + strong constraints + approval gate | Script validation + human confirmation |
| Medium | Deployment, bulk refactoring, config changes | Pseudocode + parameterized steps | dry-run + checkpoints |
| Low | Code review, copy-polish, info summarization | Natural-language instructions, leave room for judgment | Output format conventions |
In short, it comes down to how expensive a mistake in this step is:
-
Irreversible → script it + require approval;
-
Reversible but wide blast radius → dry-run + checkpoints;
-
Fine to tweak freely → give a direction and let it improvise.
Anthropic expressed a similar view in Building Effective Agents: use workflows for what predefined code paths can solve, and leave flexibility to where the model genuinely needs dynamic decisions. Over-agentification is itself an anti-pattern.
5. L3: Guardrails
Turning "should" into "checkpoints"
A heads-up: the checks:, side_effects:, on_failure: fields in this chapter are not part of the official agent-skills frontmatter — no runtime currently auto-parses or executes them. They are an explicit contract: written out clearly so the agent can follow them, scripts can enforce them, and humans can review them. To make them actually take effect, you have to implement the corresponding logic in scripts/ or in the body.
5.1 The "thirty percent" principle
My experience: in a mature Skill, at least 30% of the constraints should be turned into machine-verifiable checkpoints.
Natural language suits semantic judgments that can't be exhaustively enumerated; code suits format, numeric, existence, and idempotency checks.
❌ Anti-example
Be careful not to leak secrets, and validate input parameters.
✅ Good example
checks:
\- id: secret-leak-scan
run: ./scripts/scan\_secrets.sh
on\_failure: block # halt immediately
\- id: version-format-check
run: ./scripts/semver\_check.sh "\${image\_tag}"
on\_failure: block
\- id: idempotency-rehearsal
run: ./scripts/dry\_run.sh
on\_failure: ask\_user # stop and ask
Note that on_failure has three possible values, mapping to three handling strategies: block (halt immediately), ask_user (stop and ask), retry (can retry). Writing the handling strategy in as well is what makes the Skill complete.
5.2 Declare a side-effect contract
If a Skill produces external side effects, it's best to write them down clearly:
inputs:
service: string # service name
image\_tag: semver # image version
namespace: enum(prod-us, prod-eu)
side\_effects:
\- runs kubectl apply against the target namespace
idempotent: true # whether the operation is idempotent
dry\_run: scripts/dry\_run.sh # rehearsal script
rollback: scripts/rollback.sh # rollback script
requires\_approval: true # whether human approval is required
Idempotency is especially important, because an agent may retry in all sorts of situations: network jitter, tool timeouts, replanning after context compression. If a Skill isn't idempotent and you don't state it, a single retry can turn into a double deployment.
5.3 Failure strategies should be tiered
Blindly "retry three times on failure" is not a good failure-handling strategy.
retry:
max: 1
conditions: \[network-timeout] # only retry transient failures
escalate:
conditions: \[insufficient-permission, data-corruption-risk] # structural errors go up immediately
human\_approval:
conditions: \[production-write, rollback-operation]
never\_skip: \[preflight, threshold-triggered-rollback] # these steps are never skipped
Retries apply only to problems provably transient; insufficient permissions, data-corruption risk, and unknown states must stop or escalate — retrying does nothing useful there.
5.4 One hidden runtime constraint
If a Skill has any stage that waits for human input, authorization confirmation, or an external callback, you must declare whether it can be used inside a sub-agent.
I once hit an example: when the Skill was invoked in a sub-agent, the interactive authorization prompt was returned verbatim to the orchestrator — but at the time there was no primitive that let the orchestrator inject a reply and resume the sub-agent session. The flow dead-locked.
So before publishing, you must identify whether the Skill depends on interactive gating and declare it as either "top-level session only" or "usable in sub-agents".
6. When to Split, When to Merge
6.1 Split criterion: look at the "reason for change", not the length
The most common mistake is splitting by file length — e.g., cutting SKILL.md whenever it exceeds 500 lines — and that's almost always wrong.
Better question: would these two parts change for different reasons?
| Judgment dimension | Conclusion |
|---|---|
| Different trigger conditions | Split |
| Different verification standards | Split |
| Different permission/approval requirements | Split |
| Different input/output contracts | Split |
| Just longer steps | Don't split |
| Only different tools called, same flow | Parameterize |
| Different output format but same flow | Add a template |
In short: whether to split depends on whether the "reasons for change" — trigger, verification, permissions — differ, not on length.
6.2 The boundary between Skill / tool / RAG
Here's a table I've put together:
| Asset | Core responsibility | Typical failure | Governance measure |
|---|---|---|---|
| Skill | Steps, constraints, decisions, acceptance | Wrong trigger, step drift, missing acceptance | description, manifest, eval |
| tool/function | Operation interface & side effects | Parameter errors, insufficient permission, exceptions | schema, error codes, idempotency, logging |
| RAG/knowledge base | Volatile facts | Retrieval errors, stale versions | Source metadata, citation validation |
| Memory | User & task state | Overreach, staleness, pollution | Scoping, TTL, write approval |
-
"Can it be called" problems → look at the tool,
-
"Are the facts right" problems → look at RAG,
-
"Is the behavior stable" problems → look at the Skill.
A word on MCP while I'm at it: MCP solves "where to connect"; Skills solve "how to use it well". They complement each other — they're not rivals.
7. No Eval, No Skill Iteration
Most people finish writing a Skill, run it twice, feel fine, and submit. Three months later, nobody dares to touch it — because they don't know whether a change will break something.
7.1 The four metrics must be tested separately
Evaluating only the final answer mixes four kinds of problems together. I recommend splitting them:
| Metric | Definition | What it diagnoses |
|---|---|---|
| Routing accuracy | Samples where the correct first choice or candidate was selected ÷ routing samples | Trigger & boundaries |
| Step-following rate | Samples where mandatory steps were followed ÷ samples correctly routed to this Skill | SOP expression |
| Checkpoint pass rate | Trials where automatic checkpoints passed ÷ total trials | Hard constraints |
| Task success rate | Trials satisfying final environment assertions ÷ total trials | Overall result |
A drop in the metrics points directly at what to fix:
-
Routing accuracy down → fix the
description; -
Step-following rate down → fix the body instructions;
-
Checkpoint pass rate down → fix the scripts or validation logic;
-
Everything down and unfixable no matter what → the Skill needs a redesign.
7.2 A minimal usable routing test set
Routing is the easiest to test and the first thing you should test. I suggest maintaining 20 samples per Skill:
| Type | Count | Purpose |
|---|---|---|
| Typical positive examples | 5 | Verify the core task can trigger |
| Near-synonym rewrites | 5 | Verify robustness against natural expression drift |
| Strong negatives | 5 | Verify exclusion clauses actually take effect |
| Adjacent-skill boundary | 5 | Verify discrimination between candidates |
The "strong negatives" group is especially valuable — it tests exactly whether what you wrote under "DO NOT USE FOR" actually works.
7.3 Don't just look at the final output
Anthropic stressed a key point when discussing agent eval: the model saying "booking confirmed" doesn't mean a booking record actually exists in the environment.
So assert against environment state, not model output:
❌ Anti-example: only checks what the model said
assert "released" in final\_output
✅ Good example: checks the real state of the environment
assert query\_deployment("payments-api").version == "1.42.0"
assert query\_slo("payments-api").error\_budget\_percent > 5
Also, run each group at least 3 trials. Agent output fluctuates; a single run proves nothing.
7.4 Move from "tweaking the prompt" to "reading the trace"
When you don't know where a Skill is going wrong, read the trace first — don't rewrite the wording on a hunch.
A useful trace should contain at least:
-
the Skill's id and version number;
-
all candidates at trigger time and the selection reason;
-
which part of the body was loaded;
-
script calls, arguments, and output;
-
checkpoint results;
-
rollback events.
Debugging flow:
-
Reproduce: pin the prompt, model, tool state, and environment variables;
-
Layer replay: first mock the tools to verify step logic, then hook up the real environment to verify side effects;
-
Diff comparison: put a successful and a failed trace side by side to locate where the divergence is — routing, arguments, branch, or checkpoint;
-
Minimal fixes: change only one category at a time (description, body, script, data), then re-run the same eval set.
If you change four things at once, even a good result won't tell you which one earned it — and a bad result is worse, because you'll have to change them back.
8. Versioning: Making Skills Maintainable
Once you're in a team, a Skill is no longer a "conversation attachment" — it's a code asset.
Recommended directory structure:
skills/
release-canary/
SKILL.md # metadata + instructions + examples
CHANGELOG.md
metadata.schema.json
scripts/
preflight.sh # preflight
dry\_run.sh # rehearsal
rollback.sh # rollback
tests/
smoke.yaml
eval/
001-canary-normal-path.yaml
002-slo-exceeded-triggers-rollback.yaml
references/
release-policy.md # release policy details
Version numbers should follow major.minor.patch:
| Change type | Version rule | Example |
|---|---|---|
| Trigger condition or contract change | major | Removing auto-scale, changing required inputs |
| Added step or check | minor | Adding an SLO budget check |
| Copy or example revision | patch | Adjusting the description, adding examples |
Mark breaking changes explicitly in the CHANGELOG:
v2.0.0 - 2026-08-20
\- BREAKING: removed auto-scale; callers must explicitly approve
\- description: added exclusion clause for "ad-hoc troubleshooting/debugging" scenarios
\- checks: require SLO budget validation before scaling up
Without versions and a CHANGELOG, you can't attribute a failure to "this change broke it". And attribution is the prerequisite for continuous iteration.
9. A Checklist You Can Copy Directly
Run through the following before publishing a Skill:
Trigger (L1)
-
[ ]
namefollows the platform naming convention (lowercase letters, digits, hyphens; matches the directory name) -
[ ]
descriptionstates all of "what it does, when to use, when not to use" -
[ ] Exclusion clauses point to "who to go to", not just "don't come to me"
-
[ ] "USE WHEN" contains the user's real phrasings, not bookish terminology
-
[ ] Confirmed the Skill is actually scanned at runtime (not just present on disk)
Body (L2)
-
[ ] Has explicit "non-goals", delineating responsibility boundaries
-
[ ] "Prerequisites" make clear "stop and ask when conditions aren't met"
-
[ ] Steps are numbered, each with a stop condition
-
[ ] Examples and counter-examples come in pairs; counter-examples cover the most easily confused neighboring Skill
-
[ ] No piling of strong imperative words; key rules carry a "why"
-
[ ] Body is under 500 lines; long material already split into
references/
Execution & guardrails (L3)
-
[ ] At least 30% of constraints turned into machine-verifiable checkpoints
-
[ ] Side effects, idempotency, rehearsal script, and rollback script are declared
-
[ ] Failure strategy is tiered: transient failures retry, structural errors escalate
-
[ ] If it depends on interactive gating, declared whether sub-agents are supported
Evaluation
-
[ ] Has 20 routing samples (including 5 strong negatives)
-
[ ] Assertions hit environment state, not model output
-
[ ] At least 3 trials per group, with baseline comparison
-
[ ] Trace is replayable; can locate exactly which step failed
-
[ ] Has version number and CHANGELOG; breaking changes marked
Honestly, I think all of the above (even this whole article) could itself be written as a Skill for checking Skills — haha!