Build AI Skills for Any SDK, Package or Framework: A Claude Skills Deep Dive
Tutorials Claude Agent Skills AI Coding Tools Capacitor Zod

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

D. Rout

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:

  1. Simple: a one-file skill that stops Claude writing Zod 3 code in a Zod 4 project.
  2. 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.swift and build.gradle in 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

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.

  1. 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.
  2. 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.
  3. 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:

  1. Metadata (name + description): always in context, for every conversation.
  2. SKILL.md body: read when the description matches.
  3. 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.md or 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-conventions mega-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

  1. Package both skills as a Claude Code plugin so a team installs them with one command and gets updates.
  2. 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.
  3. Automate the evals in GitHub Actions against the API so a skill regression fails CI like a unit test would.
  4. Write a skill for your own SDK: point the probe approach at the three things new users always get wrong.

Further reading

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.

Share

Comments (0)

Join the conversation

Sign in to leave a comment on this post.

No comments yet. to be the first!