How-to › Use AI to do the integration

How to install an API skill and check a coding agent uses it#

Install a vendor's published SKILL.md with its digest verified, lint the description the agent matches tasks against, and confirm the agent loads it.

Audience
API consumer
Level
beginner
Topic
Write an integration with a coding agent
Verified

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

ApproachWhen it fitsWhat it costs youWhen to pick something else
Agent Skills (SKILL.md)The vendor publishes one, and the API comes up in some tasks rather than allOnly the name and description load until it fires, so a vague description means it never firesEvery task in the repository touches the API
Cursor rulesCursor, with guidance that should apply to every chat or to matching filesAn alwaysApply rule spends its whole body on every turn, needed or notYou want one file that more than one agent reads
llms.txt in contextAny agent, no install step, and a vendor who keeps the file currentAn index of links rather than instructions, so the agent still decides how to callYou need a rule such as “use the client, never raw fetch” followed
sdkgen generated agent filesYou maintain an SDK generated by sdkgen and want agents to edit the modelAGENTS.md guides for the SDK’s maintainers, each with a CLAUDE.md pointing to it, and no SKILL.mdYou 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.

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

  1. 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

  2. The specification caps a description at 1,024 characters. Claude Code cuts each listing entry at 1,536, counting description together with when_to_use, a field the specification does not define. A description at the specification’s limit leaves when_to_use 512 characters before the listing is cut. ↩︎ Back to text

  3. 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

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Listen to Fireside, the Voxgig podcast

Since 2018, Richard Rodger has talked with developer advocates, event organizers, and the people who build developer-facing companies about the specifics of the work.