You installed the vendor’s skill for its metering API, asked the agent for a script that reads
meter readings, and got raw fetch calls to a path the API does not have. The skill never
loaded. Its description welcomes a reader to the platform without once naming the API, so the
agent had nothing to match the task against.
What you get
You will end up with a skill installed where Claude Code and Cursor both look, its bytes checked against the publisher’s digest, and its description checked for the words a task uses. This is for you if you call an API whose vendor publishes agent instructions.
Short answer
Put the vendor’s SKILL.md at .claude/skills/<name>/SKILL.md, which Claude Code reads and
Cursor also loads. Install it from the vendor’s /.well-known/agent-skills/index.json and
refuse any file whose SHA-256 differs from the index. Check that the frontmatter parses and
that the description names the API, because the agent sees only the description until it
decides. Then ask the agent which skills it has, and diff its code with the skill denied.
You will need
Node 22 or later, and a coding agent installed and authenticated, such as
Claude Code or
Cursor. Verified 2026-10-15 against Node 22.22.0 and yaml
2.9.1. The metering API, @meterco/sdk, is a stand-in served from a local port. The skill
format, the index format, and the install path are the real ones. Writing a skill is the publisher’s half of the job,
which organizing skills across repositories
covers.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
Agent Skills (SKILL.md) | The vendor publishes one, and the API comes up in some tasks rather than all | Only the name and description load until it fires, so a vague description means it never fires | Every task in the repository touches the API |
| Cursor rules | Cursor, with guidance that should apply to every chat or to matching files | An alwaysApply rule spends its whole body on every turn, needed or not | You want one file that more than one agent reads |
| llms.txt in context | Any agent, no install step, and a vendor who keeps the file current | An index of links rather than instructions, so the agent still decides how to call | You need a rule such as “use the client, never raw fetch” followed |
| sdkgen generated agent files | You maintain an SDK generated by sdkgen and want agents to edit the model | AGENTS.md guides for the SDK’s maintainers, each with a CLAUDE.md pointing to it, and no SKILL.md | You call the API rather than maintain its SDK |
Voxgig maintains sdkgen. A skill and an always-on rule can carry the same text, and they
differ in when you pay for it, which the measurement further down puts in characters. llms.txt
needs nothing installed and steers least: it says where the documentation is, not what to do.
The sdkgen product page lists Agent Skills among six output surfaces, but @voxgig/sdkgen 4.34.7
writes no SKILL.md. Its source and templates hold no file of that name. An SDK generated with it
holds AGENTS.md guides at the root, per language, and per feature, each beside a CLAUDE.md
that points to it. The row describes what the package writes.
Install the skill from the publisher’s index
Claude Code reads a project skill from .claude/skills/<skill-name>/SKILL.md, and
Cursor loads that directory too for compatibility, so one
copy serves both. A vendor that publishes skills puts them behind an index at
/.well-known/agent-skills/index.json, as the
Agent Skills discovery draft
describes.1 Each entry carries a digest, and the draft says a client MUST NOT use content
that fails it.
The installer resolves the entry’s URL against the index, fetches the file, and checks the bytes before it parses anything:
const url = new URL(entry.url, indexUrl)
const res = await fetchImpl(url)
if (!res.ok) throw new Error(`GET ${url.pathname} answered ${res.status}`)
const bytes = await readCapped(res, MAX_BYTES, 'SKILL.md')
const digest = sha256(bytes)
if (digest !== entry.digest) throw new Error(`digest mismatch: index says ${short(entry.digest)}, file is ${short(digest)}`)
const report = checkSkill(bytes.toString('utf8'), { dir: name, terms })
if (report.errors.length) throw new Error(`SKILL.md fails its checks: ${report.errors.join('; ')}`)
readCapped counts the bytes as they arrive and stops reading at 256 KiB, because a chunked
response carries no Content-Length to check first. Before any fetch, the installer refuses a
skill name that is not lowercase letters, digits, and hyphens, because the name becomes a
directory. After the checks, it records the digest in
skills.lock.json, and it refuses to overwrite an installed copy that someone has edited by
hand. The demo serves the index from a local port and installs into a scratch project:
node demo.mjs
first install installed .claude/skills/meterco-api/SKILL.md
second install, same digest unchanged .claude/skills/meterco-api/SKILL.md
file changed, index did not refused: digest mismatch: index says sha256:d7c1fd26, file is sha256:bf92a10f
a name that walks out of the dir refused: refusing skill name "../../.ssh/config"
skills.lock.json records sha256:d7c1fd26
installed copy still matches index true
The third attempt is the case the digest exists for. The served file gained one line, asking the agent to send the API key to another host. The index still described the original, so the installed copy stayed the one the index lists.
Write the description for the agent
The description is the whole of what the agent reads before it chooses. Claude Code keeps every skill’s name and description in context and loads the body only when the skill is invoked.2 The specification asks for a description that says what the skill does and when to use it, with the keywords that identify the task.
The check takes the terms a task would use for this API and fails a description that names none of them:
export function lintDescription(description, terms) {
const errors = []
const warnings = []
const lower = description.toLowerCase()
const named = terms.filter((t) => lower.includes(t.toLowerCase()))
if (terms.length && !named.length) errors.push(`description names none of: ${terms.join(', ')}`)
if (!/\bwhen\b/i.test(description)) warnings.push('description never says when to use it')
return { errors, warnings }
}
Run it over the published skill and two broken ones:
node check-skill.mjs published/meterco-api/SKILL.md examples/for-humans/meterco/SKILL.md examples/colon/meterco-api/SKILL.md --terms Meterco,@meterco/sdk,api.meterco.example
ok published/meterco-api/SKILL.md
FAIL examples/for-humans/meterco/SKILL.md
error description names none of: Meterco, @meterco/sdk, api.meterco.example
warning description never says when to use it
FAIL examples/colon/meterco-api/SKILL.md
error frontmatter does not parse: Nested mappings are not allowed in compact mappings at line 2, column 14:
The second file is the problem at the top of this page. It reads well on a docs site, but it gives the agent no word to match.
The third fails on punctuation. Use when: the task mentions Meterco puts a colon and a space
inside an unquoted YAML value, so the frontmatter does not parse. Claude Code still loads a
skill whose YAML is malformed, with no fields set. /meterco-api still works when you type it,
but the agent cannot choose the skill, because it has no description to match. Quote the value
and the error goes.
Count what each option costs per turn
The same meterco-api guidance, measured four ways. “Before use” is what sits in context on every
turn until the API comes up; “after use” is what sits there once it has:
node cost.mjs
approach before use after use
Agent Skill, on demand 252 1419
Cursor rule, always 1167 1167
Cursor rule, agent-requested 99 1266
llms.txt read at session start 1118 1118
characters in context on each turn; llms.txt is the index alone, before any page it links
A skill costs a fifth of an always-on rule until it is needed. Once it fires, the body stays in context across turns, so the saving is in the sessions that never use the API. llms.txt costs as much as the rule while carrying no instruction, only links the agent may or may not fetch.3
Check it worked
The tests cover the refusals the demo shows and five cases it does not. Those are a
hand-edited copy, a deleted copy, an index over plain HTTP, and a misspelled frontmatter field.
The fifth is a 4 MiB response with no Content-Length, which the installer has to abandon at the
cap rather than read whole.
node --test skill.test.mjs
1..17
# tests 17
# suites 0
# pass 17
# fail 0
# cancelled 0
# skipped 0
# todo 0
The last two checks need a model, so this page did not run them. Run them in your own repository with your API’s skill. Start by asking the agent what it has; the Claude Code troubleshooting guide uses this exact question:
claude -p "What skills are available? Reply with their names only."
meterco-api has to be in the answer. Then give the same task twice, once with the skill
denied through a --disallowedTools rule, and
compare what was written. Start from a clean checkout, because each run is reset afterwards:
TASK="Add src/usage.ts that prints the kWh used last month by every meter on site s-1."
claude -p "$TASK" --permission-mode acceptEdits
git add -A && git diff --cached > ../with-skill.diff && git reset -q --hard && git clean -fdq
claude -p "$TASK" --permission-mode acceptEdits --disallowedTools "Skill(meterco-api)"
git add -A && git diff --cached > ../without-skill.diff && git reset -q --hard && git clean -fdq
grep -c "@meterco/sdk" ../with-skill.diff ../without-skill.diff
The skill did its job if the first diff imports @meterco/sdk and iterates pages with
for await, and the second does not. Two identical diffs mean the agent wrote from memory both
times.
When it goes wrong
The skill is installed but never fires. Its description is generic, or it was dropped. Claude Code fits the listing into a budget of 1% of the context window and drops descriptions, least-used skills first, when it overflows. Name the API in the first sentence of the description.
The skill loads in your terminal but not in CI. claude --bare skips skill discovery, as
headless mode documents, and the recommended CI
invocation uses it. Pass the directory that holds .claude/skills with --add-dir, whose
skills still load.
A Cursor rule never applies. The file is meterco.md, and
Cursor ignores a plain .md file in .cursor/rules. Rename
it .mdc.
The installer refuses on a digest mismatch every time. The vendor changed SKILL.md without
regenerating the index, or a cache serves one of the two stale. Report it to the vendor rather
than skipping the check.
When not to do this
Do not install a skill for an API that every task in the repository uses. A skill exists to stay
out of context until it is needed, and a rule in AGENTS.md costs the same once the skill fires
on every task, with no description to get wrong.
Do not read a verified digest as a safety review. It proves the file is the one the index lists, not that its instructions are ones you would accept, and a site that serves both can change both. Read a skill before you install it, as you would read a script before you pipe it to a shell.
Do not install sdkgen’s generated AGENTS.md files as a skill for calling the API. They tell an
agent how to regenerate the SDK from its model, which helps the SDK’s maintainers and misleads
anyone who only imports the package. For an sdkgen-generated client, write the skill yourself
from its README until the package ships one.
Related how-tos
Last verified
Verified 2026-10-15 against Node 22.22.0 and yaml 2.9.1. Every output block is what the command
preceding it printed. The claude commands were not run, because they need a model and an API
key. The sdkgen row comes from unpacking @voxgig/sdkgen 4.34.7 and @voxgig/create-sdkgen
0.30.7 from npm and from an SDK scaffolded and generated with them, none of which contains a
SKILL.md.
Footnotes
-
The draft hangs its index on RFC 8615, the 2019 document that reserves
/.well-known/so that sites can publish metadata without colliding with their own URLs. It replaced RFC 5785, which had reserved the same prefix in 2010. The draft’s changelog records one move already: version 0.2.0 renamed the path from/.well-known/skills/to/.well-known/agent-skills/. A location whose purpose is to be predictable has so far been predictable in two places. ↩︎ Back to text -
The specification caps a description at 1,024 characters. Claude Code cuts each listing entry at 1,536, counting
descriptiontogether withwhen_to_use, a field the specification does not define. A description at the specification’s limit leaveswhen_to_use512 characters before the listing is cut. ↩︎ Back to text -
The proposal is by Jeremy Howard and dated September 3, 2024. Its second version reports thousands of sites publishing the file, and names OpenAI, Anthropic, and Gemini among the labs that serve one for their own developer documentation. ↩︎ Back to text