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

Source: https://voxgig.com/howto/install-an-api-skill-for-a-coding-agent

- Audience: api-consumer
- Level: beginner
- Verified: 2026-10-15
- Published: 2026-10-15

## 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](https://code.claude.com/docs/en/skills) or
[Cursor](https://cursor.com/docs/skills). 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](/howto/organise-skills-across-a-multi-package-ecosystem)
covers.

## Approaches compared

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [Agent Skills (`SKILL.md`)](https://agentskills.io/specification) | 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](https://cursor.com/docs/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](https://llmstxt.org/) | 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](https://voxgig.com/sdk) | 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](https://cursor.com/docs/skills) 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](https://github.com/cloudflare/agent-skills-discovery-rfc)
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:

```js title="skill.mjs"
  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:

```bash
node demo.mjs
```

```text output
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](https://code.claude.com/docs/en/skills).[^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:

```js title="skill.mjs"
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:

```bash
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
```

```text output
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:

```bash
node cost.mjs
```

```text output
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](https://code.claude.com/docs/en/skills), 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.

```bash
node --test skill.test.mjs
```

```text output
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:

```bash
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`](https://code.claude.com/docs/en/cli-reference) rule, and
compare what was written. Start from a clean checkout, because each run is reset afterwards:

```bash
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](https://code.claude.com/docs/en/headless) 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](https://cursor.com/docs/rules) 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

- [Organize agent skills across many related repositories](/howto/organise-skills-across-a-multi-package-ecosystem)

- [Stop a coding agent inventing endpoints not in the spec](/howto/stop-a-coding-agent-inventing-endpoints)

- [Write a task prompt that gets an API integration done](/howto/write-a-task-prompt-for-an-api-integration)

- [Make a Docusaurus site agent-ready](/howto/make-a-docusaurus-site-agent-ready)

- [Run a coding agent headless in CI to keep a client current](/howto/run-a-coding-agent-headless-in-ci)

## 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`.

[^1]: The draft hangs its index on [RFC 8615](https://www.rfc-editor.org/info/rfc8615/), the
2019 document that reserves `/.well-known/` so that sites can publish metadata without
colliding with their own URLs. It replaced [RFC 5785](https://www.rfc-editor.org/info/rfc5785/),
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.

[^2]: The [specification](https://agentskills.io/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.

[^3]: The proposal is [by Jeremy Howard](https://llmstxt.org/) 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.