
Build AI Skills for Any SDK, Package or Framework: A Claude Skills Deep Dive

D. Rout
September 28, 2026 18 min read
On this page
GitHub Repo: Every skill, script and fixture in this tutorial is in claude-skills-starter. Clone it, run
npm test, and follow along.
Introduction
Your AI assistant knows the internet as it was when it was trained. That is fine for Array.prototype.map, and a problem for anything that moves: the SDK that shipped a v4 last quarter, the podspec your team generates a particular way, the framework whose "right way" changed twice this year. You end up pasting the same corrections into every chat.
A skill fixes that once. It is a folder with a SKILL.md file (plus optional reference docs and scripts) that an agent loads only when the task calls for it. The format started at Anthropic and is now an open standard supported by Claude, Claude Code, Cursor, GitHub Copilot, Gemini CLI, Codex and many others, so a skill you write for one tool mostly works in the rest.
This post has two halves. First, a tool-agnostic method for turning any SDK, npm package, podspec or framework into a skill. Then a deep dive on Claude specifically, with two worked examples:
- Simple: a one-file skill that stops Claude writing Zod 3 code in a Zod 4 project.
- Intermediate to advanced: a multi-file skill with progressive disclosure and a validation script that keeps a Capacitor 8 plugin's
package.json, podspec,Package.swiftandbuild.gradlein sync.
Along the way you get something the official docs do not give you: a measured list of Zod 3 habits that fail silently in Zod 4, a skill linter, and real token numbers for what each skill costs your context window.
Prerequisites
- Node.js 20 or later
- One of: Claude Code, a Claude paid plan with code execution enabled, or an Anthropic API key
- Basic familiarity with Claude. If you are new, start with Claude AI: The Complete Developer Guide
- For the advanced example, a rough idea of how Capacitor plugins are laid out. Building a Capacitor Plugin From Scratch covers it
Part 1: The method, for any SDK, package or framework
Skills vs prompts vs MCP
Before writing anything, check that a skill is the right tool.
| You need the agent to... | Use | Why |
|---|---|---|
| Follow a one-off instruction | A prompt | Nothing to reuse |
| Call a live system (API, database, CMS) | An MCP server | MCP gives tools; it does not teach judgment |
| Know how your version of a library works, or follow a repeatable procedure | A skill | Loaded on demand, versioned in git, portable |
| Both of the last two | A skill that references MCP tools | The skill says when and how; MCP does the calling |
The three gaps a skill can close
Every useful skill closes one or more of these gaps. Name yours first, because it decides what goes in the folder.
- Knowledge gap. The model's training data is out of date or too generic. Examples: a major version bump (Zod 4, Angular Signal Forms), your internal SDK. Fix: rules and examples in
SKILL.md. - Procedure gap. The model knows the pieces but not your order of operations. Examples: release a library, cut a podspec version, migrate a database. Fix: a numbered workflow with a checklist.
- Verification gap. Mistakes are easy and silent. Examples: three manifests that must agree, a generated file that must validate. Fix: a script the agent runs, not reads.
What goes into the folder, by target type
| Target | Typical gap | Put in SKILL.md |
Put in reference/ |
Put in scripts/ |
|---|---|---|---|---|
| npm package after a major release | Knowledge | "Don't write X, write Y" table, one canonical example | Full API diff | Version check |
| Mobile SDK / podspec / Gradle | Procedure + verification | Workflow, which files a change touches | Per-platform field notes | Manifest cross-checker |
| Framework (Angular, Rails, Spring) | Knowledge + procedure | Project conventions, generator commands | Per-feature guides | Lint or build wrapper |
| Internal REST API | Knowledge | Auth pattern, error shape, two examples | Endpoint reference per domain | Contract test |
Step 1: Measure the gap before you write a word
The best skills are written against observed failures, not imagined ones. For the simple example I installed the current Zod (4.6.5) and ran the Zod 3 habits most code on the internet still uses:
// examples/zod-demo/probe-v3-habits.mjs (excerpt)
import { z } from "zod";
const first = (schema, input) => {
const r = schema.safeParse(input);
return r.success ? "(parsed OK)" : r.error.issues[0].message;
};
console.log(first(z.string({ invalid_type_error: "must be string" }), 1));
console.log(first(z.string({ required_error: "req!" }), undefined));
console.log(first(z.string({ errorMap: () => ({ message: "custom" }) }), 1));
console.log(first(z.string({ error: (iss) => iss.input === undefined ? "required" : "must be string" }), 1));
The results, run on Node 22 with Zod 4.6.5:
| Zod 3 habit | Result in Zod 4.6.5 | Severity |
|---|---|---|
invalid_type_error: "must be string" |
Shows "Invalid input: expected string, received number" | Silent: your message is dropped, no warning |
required_error: "req!" |
Shows "Invalid input: expected string, received undefined" | Silent |
errorMap: () => ({ message: "custom" }) |
Shows the default message | Silent |
z.string().email({ message: "bad" }) |
Shows "bad" | Deprecated, still works |
.strict(), .merge(), z.nativeEnum |
All still work | Deprecated, still works |
error: (iss) => ... (the v4 way) |
Shows "must be string" | Correct |
This is the most useful table in the post. The deprecated APIs are an annoyance; the three silent ones ship broken validation messages to production with green tests. That tells us exactly what the skill must prioritise.
Gotcha I hit: my first draft of this skill also said "z.record() requires two arguments in v4." The probe proved that one-argument z.record(z.string()) parses fine in 4.6.5. A rule that is wrong is worse than no rule, because the agent will "fix" working code. Run every rule you write against the version you actually ship.
Part 2: Claude Skills, the simple example
Step 2: Write the SKILL.md
A skill is a folder whose name matches the name field. Create skills/writing-zod-v4-schemas/SKILL.md:
---
name: writing-zod-v4-schemas
description: Writes and reviews Zod 4 validation schemas in TypeScript, avoiding Zod 3 APIs that are silently ignored or deprecated in v4. Use when writing or editing Zod schemas, when code imports from "zod", mentions z.object, safeParse or z.infer, or when upgrading a project from Zod 3 to Zod 4.
---
# Writing Zod 4 schemas
## Step 1: confirm the version
node -p "require('zod/package.json').version"
If it prints 3.x, stop and tell the user this skill targets Zod 4.
## Step 2: never use these (silently ignored in v4, no error thrown)
| Zod 3 param | Write instead |
| invalid_type_error | error: (iss) => "..." |
| required_error | error: (iss) => iss.input === undefined ? "Required" : "..." |
| errorMap | error: (iss) => "..." |
## Step 3: prefer the v4 form over deprecated APIs
(z.email() over z.string().email(), .extend(B.shape) over .merge(B), ...)
## Pattern to follow
(one complete, runnable schema)
## Checklist before returning code
- [ ] No invalid_type_error, required_error, or errorMap anywhere
The full file is in the repo; it is 60 lines. Three decisions matter more than the rest:
The description does all the routing. At startup Claude loads only the name and description of every installed skill. The body is invisible until the description matches. So the description states what (third person: "Writes and reviews...") and when ("Use when... imports from "zod", mentions z.object, safeParse..."). Those trigger words are the literal strings that appear in a Zod task.
Only write what Claude doesn't know. There is no paragraph explaining what schema validation is. Every line is either a rule the model gets wrong or an example of the right shape.
End with a checklist. Claude treats a checklist as a self-review step before returning code. It is the cheapest feedback loop you can add.
Step 3: Install it where you use Claude
Claude Code. Copy the folder into ~/.claude/skills/ (all projects) or .claude/skills/ (this repo, shared with your team through git). Claude Code reloads skills live, so there is no restart:
cp -r skills/writing-zod-v4-schemas ~/.claude/skills/
# then, in Claude Code:
# /writing-zod-v4-schemas <- invoke explicitly
# "add a zod schema for signup" <- or let the description trigger it
Claude apps (web and desktop). Zip the folder, not its contents, and upload it under Settings → Capabilities. Code execution must be on.
npm run package -- skills/writing-zod-v4-schemas
# ✓ writing-zod-v4-schemas [claude-ai]: valid
# Created dist/writing-zod-v4-schemas.zip
# writing-zod-v4-schemas/
# writing-zod-v4-schemas/SKILL.md
Claude API. Upload once, then reference the skill by ID in the container parameter. Skills run inside the code execution tool, so include it:
import anthropic
from anthropic.lib import files_from_dir
client = anthropic.Anthropic()
skill = client.skills.create(files=files_from_dir("skills/writing-zod-v4-schemas"))
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=4096,
container={"skills": [{"type": "custom", "skill_id": skill.id, "version": "latest"}]},
tools=[{"type": "code_execution_20250825", "name": "code_execution"}],
messages=[{"role": "user", "content": "Write a Zod schema for a signup form."}],
)
The API allows up to 20 skills per request and 30 MB per uploaded skill. The API container has no network access, so a skill cannot npm install there; bundle what it needs.
Step 4: Lint the skill before you upload it
Upload errors are terse, and some rules differ by surface, so the repo ships tools/validate-skill.mjs. Here it is against a deliberately broken skill:
$ node tools/validate-skill.mjs bad/claude-Helper
warn description should be third person ("Processes X..."), not I/you
warn description has no trigger clause (add "Use when ...")
warn description is only 27 chars; it is the only thing Claude sees before loading the skill
ERROR name "claude-Helper" must be lowercase letters, numbers and hyphens only
ERROR name "claude-Helper" contains a reserved word ("anthropic" or "claude")
ERROR description contains an XML/HTML tag (angle brackets are rejected)
ERROR Windows-style backslash path in body; use forward slashes
ERROR links to more.md, which does not exist
✗ claude-Helper [claude-ai]: 5 error(s)
Notice the reserved word: the repo is called claude-skills-starter, but no skill inside it can have "claude" in its name.
Part 3: The advanced example, a Capacitor plugin skill
A Capacitor plugin publishes one version through three package managers: npm, CocoaPods (the podspec) and Swift Package Manager, plus a Gradle module for Android. (If podspecs and SPM are new to you, see Shipping Code to the World: iOS & macOS Library Distribution.) The classic failure is drift: someone bumps package.json and the podspec still says the old version, or the podspec never makes it into the npm tarball and every consumer hits No podspec found.
This is a procedure gap and a verification gap, so the skill needs more than rules.
Step 5: Lay out the folder for progressive disclosure
skills/maintaining-capacitor-plugins/
├── SKILL.md # workflow, "which files" table, target values
├── reference/
│ ├── podspec.md # loaded only when touching the podspec
│ ├── spm.md # loaded only when touching Package.swift
│ └── android.md # loaded only when touching Gradle
└── scripts/
└── check_plugin.mjs # executed, never loaded into context
Claude reads the three levels at different times:
- Metadata (name + description): always in context, for every conversation.
- SKILL.md body: read when the description matches.
- Reference files and scripts: read or run only when the body points to them.
Two rules keep this working. Link every reference file directly from SKILL.md: Claude may only preview a file that is linked from another linked file, so nested references get half-read. And tell Claude whether to run a script or read it. "Run the checker (do not read its source)" means 1,300 tokens of JavaScript never enter the context; only its output does.
Step 6: Write the workflow around the script
The heart of the advanced SKILL.md is a copyable checklist and one command:
## Workflow
Copy this checklist into your response and tick it off:
```
Plugin task progress:
- [ ] 1. Run the checker to get a baseline
- [ ] 2. Make the requested change
- [ ] 3. Apply the same change to every manifest it touches (see table)
- [ ] 4. Re-run the checker; fix and repeat until it prints PASS
- [ ] 5. Summarise what changed per platform
```
**Step 1 and 4: run the checker** (do not read its source, just run it):
```bash
node ${CLAUDE_SKILL_DIR}/scripts/check_plugin.mjs <plugin-root>
```
Outside Claude Code, use the path of this skill folder instead of `${CLAUDE_SKILL_DIR}`.
${CLAUDE_SKILL_DIR} is a Claude Code substitution that expands to the skill's folder. Other surfaces don't expand it, which is why the next line gives a fallback. That one sentence is the difference between a skill that works in Claude Code only and one that works everywhere.
The body also carries a "which files does this change touch" table and the Capacitor 8 target values (iOS 15.0, .iOS(.v15), minSdk 24, compileSdk 36, @capacitor/core >=8.0.0), taken from the official Capacitor 8 plugin upgrade guide.
Step 7: Make the script specific enough to act on
The checker cross-reads all four manifests and prints one line per problem with the file, the value found and the value expected. The repo includes a healthy plugin and a realistic "half-upgraded from Capacitor 7" copy:
$ node skills/maintaining-capacitor-plugins/scripts/check_plugin.mjs examples/echo-plugin
PASS capacitor-echo@1.4.0 (npm, CocoaPods, SPM and Gradle agree)
$ node skills/maintaining-capacitor-plugins/scripts/check_plugin.mjs examples/echo-plugin-drifted
FAIL 7 problem(s) in capacitor-echo@1.5.0:
- package.json: peerDependencies["@capacitor/core"] is ">=7.0.0", expected ">=8.0.0"
- package.json: "files" does not include CapacitorEcho.podspec (pod install will fail with "No podspec found")
- CapacitorEcho.podspec: s.version hard-coded to 1.4.0, package.json is 1.5.0
- CapacitorEcho.podspec: ios.deployment_target is 14.0, expected 15.0
- Package.swift: capacitor-swift-pm from: "7.x", expected "8.0.0"
- android/build.gradle: minSdkVersion fallback is 23, expected 24
- android/build.gradle: compileSdk fallback is 35, expected 36
Each line is an instruction Claude can act on without reading any reference file. That is the "solve, don't defer" principle: a script that just throws Error: invalid hands the problem back to the model, and the model will guess.
A trimmed excerpt of the podspec check:
const ver = pod.match(/s\.version\s*=\s*(.+)/)?.[1]?.trim();
if (ver && /^['"]/.test(ver)) {
const hard = ver.replace(/['"]/g, "");
if (hard !== pkg.version) add(podspecName, `s.version hard-coded to ${hard}, package.json is ${pkg.version}`);
else add(podspecName, `s.version is hard-coded; use package['version'] to prevent future drift`);
}
if (!inFiles(podspecName)) {
add("package.json", `"files" does not include ${podspecName} (pod install will fail with "No podspec found")`);
}
Step 8: Measure what the skill costs your context
Progressive disclosure is a claim; tools/token-budget.mjs measures it. It estimates tokens at about four characters per token (use Anthropic's token-counting endpoint for exact figures) for each level:
| Skill | L1 metadata (always) | L2 body (on trigger) | L3 references (on demand) | Scripts (run, not loaded) | If you pasted it all |
|---|---|---|---|---|---|
| writing-zod-v4-schemas | 82 | 651 | 0 | 0 | 733 |
| maintaining-capacitor-plugins | 101 | 642 | 916 (3 files) | 1,324 | 2,983 |
What the numbers say:
- The always-on cost is tiny. Twenty skills like these cost about 2,000 tokens of standing context. That is the real reason to prefer skills over a giant
CLAUDE.mdor system prompt. - A typical Capacitor task costs about 1,050 tokens, not 2,983: metadata, body, and the one reference file for the platform being edited. The script's 1,324 tokens never load.
- The body is the budget to watch. Both bodies land around 650 tokens. If yours passes 2,000, move tables into
reference/.
Step 9: Write evals, then test on every model you use
Three scenarios per skill is the minimum. The repo's evals/ folder uses the format from Anthropic's authoring guide:
{
"skills": ["writing-zod-v4-schemas"],
"query": "Review this schema: z.string({ required_error: 'Name is required' })",
"expected_behavior": [
"States that required_error is silently ignored in Zod 4",
"Rewrites it with error: (iss) => iss.input === undefined ? ... : ..."
]
}
Run each query with the skill off to get a baseline, then with it on. The loop I use is the one from automated evals in the LLMs in Production series: one Claude session edits the skill, a fresh session runs the evals, and you carry observations back. Test on the smallest model you plan to use. Opus often infers what a skill leaves implicit; Haiku may need the rule spelled out.
Reference: limits and fields by surface
| Item | Claude Code | Claude apps | Claude API |
|---|---|---|---|
| Where skills live | ~/.claude/skills/, .claude/skills/, plugins |
Uploaded zip | client.skills.create() |
name |
Optional (defaults to folder name) | Required, ≤ 64 chars, a-z0-9- |
Same as apps |
Reserved words in name |
n/a | claude, anthropic |
claude, anthropic |
description |
≤ 1,536 chars with when_to_use |
≤ 1,024 chars, no angle brackets | Same as apps |
| Extra frontmatter | allowed-tools, disable-model-invocation, context: fork, paths, model |
Ignored | Ignored |
${CLAUDE_SKILL_DIR} |
Expanded | Not expanded | Not expanded |
| Network in scripts | Your machine | Can install npm/PyPI packages | None |
| Size limit | Your disk | 30 MB | 30 MB, 20 skills per request |
| Recommended body length | < 500 lines | < 500 lines | < 500 lines |
Common errors and fixes
| Symptom | Cause | Fix |
|---|---|---|
| Skill never triggers | Description too vague or missing trigger words | Add "Use when..." with the literal terms users type |
| Triggers on unrelated tasks | Description too broad ("Helps with code") | Name the file types, libraries or commands |
| Upload rejected | Reserved word, uppercase, or < in description |
Run validate-skill.mjs first |
| Claude reads half a reference file | Reference linked from another reference | Link every file from SKILL.md directly |
| Script path not found outside Claude Code | Used ${CLAUDE_SKILL_DIR} with no fallback |
Add a one-line fallback instruction |
| Claude "fixes" working code | A rule in the skill is wrong for your version | Probe every rule against the installed version |
My recommendations
- Start from a failure, not a topic. "Zod 4 skill" is a topic. "Claude drops our error messages" is a failure. Only the second produces a skill worth loading.
- One skill per package or workflow. A
frontend-conventionsmega-skill triggers on everything and helps with nothing. - Prefer a script over a paragraph whenever the check is mechanical. Seven lines of checker output beat a 200-line reference file.
- Commit project skills.
.claude/skills/in the repo means every teammate and CI agent gets the same behaviour, and skill changes go through code review.
What's next
- Package both skills as a Claude Code plugin so a team installs them with one command and gets updates.
- Pair a skill with an MCP server. The MCP server gives Claude the tools (for example, creating posts in a CMS); a companion skill encodes the house style and SEO checklist for using them well.
- Automate the evals in GitHub Actions against the API so a skill regression fails CI like a unit test would.
- Write a skill for your own SDK: point the probe approach at the three things new users always get wrong.
Further reading
- Agent Skills specification: the open format, field by field
- Skill authoring best practices: Anthropic's guide to descriptions, disclosure and evals
- Skills in Claude Code: frontmatter fields, arguments and invocation control
- Using Skills with the Claude API: upload, versioning and the container parameter
- Zod 4 migration guide: the full list of v3 to v4 changes
- Updating plugins to Capacitor 8: the source of the checker's target values
Wrapping up
A skill is the smallest unit of "teach the agent once." For any SDK, package or framework, the method is the same: measure the gap, write only what the model gets wrong, put mechanical checks in a script, and measure the context cost. The Zod skill shows that a single 60-line file can prevent a class of silent production bugs; the Capacitor skill shows how references and a checker scale that to multi-platform workflows.
Everything here, including the probe, the linter, the token-budget tool, both fixtures and the evals, is in claude-skills-starter. Fork it, run npm test, and swap in the library your team fights with most.
Read next
Comments (0)
Join the conversation
Sign in to leave a comment on this post.
No comments yet. to be the first!