Skip to main content
BLOG

Agent Skills SKILL.md for websites: write, index, verify

Write an Agent Skills SKILL.md for your site, list it in /.well-known/agent-skills/index.json, and verify its sha256 digest with one curl script.

By IMozzUpdated 2026-11-01
Agent Skills SKILL.md for websites — aiSiteReady

An Agent Skill for a website is a SKILL.md file that teaches an agent how to use your site or API, plus an entry in /.well-known/agent-skills/index.json that lets agents find the file and check that it hasn't changed. The file format comes from agentskills.io. The discovery index is a separate draft with five required fields per skill, and the field that is easiest to get silently wrong is digest: a SHA-256 hash of the exact bytes you serve.

Skills are the last layer of the readiness stack. An agent first reaches and reads your pages, then finds what it can call, then learns how to use it well. Make your site agent-callable draws that whole map, with the MCP Server Card saying where to connect and the API catalog listing your APIs.

This post covers that layer in detail. One caveat first: the SKILL.md format is widely supported, but the website index is newer, less widely supported, and still a draft. I could verify one open-source client that reads it.

Key takeaways

  • A skill is a SKILL.md with YAML front-matter: a name (max 64 characters, lowercase letters, digits, hyphens) and a description (max 1,024 characters) that says what it does and when to use it (Agent Skills specification).
  • Website discovery is a draft, not an IETF RFC: Agent Skills Discovery 0.2.0, updated 12 March 2026. The index lists skills with name, type, description, url, and digest (cloudflare/agent-skills-discovery-rfc).
  • digest is sha256: plus the hash of the artifact's raw bytes. Clients MUST verify it and MUST NOT use content that fails (same draft).
  • The cheapest way to avoid drift is to compute the digest from the same bytes the route serves. That's how aiSiteReady does it, and I checked the result from outside with shasum.
  • aiSiteReady's agent_skills check validates the index shape and the digest format. It never downloads the SKILL.md, so run the verification script below yourself.

What is an Agent Skill, and how is it different from API docs?

An Agent Skill is a folder with a required SKILL.md (YAML front-matter plus Markdown instructions) and optional scripts/, references/, and assets/ directories (Agent Skills specification). Unlike API docs, which say what exists and where, it tells an agent what to do. Anthropic originally developed the format and released it as an open standard; the client list on agentskills.io now shows more than forty supporting products.

It scales through progressive loading. At startup an agent reads only each skill's name and description, about 100 tokens per skill. When a task matches, it loads the SKILL.md body, under 5,000 tokens. Bundled files load only if the instructions point to them (Anthropic docs).

That separates a skill from API documentation. OpenAPI and a Server Card tell an agent what exists and where. A skill tells it what to do: when it applies, which calls to make in what order, and what to do on failure. It declares no transport or endpoint of its own.

Front-matter fieldRule
name (required)Max 64 characters; lowercase letters, digits, hyphens; no leading, trailing, or double hyphen; must match the parent directory name
description (required)1 to 1,024 characters; says what the skill does and when to use it

The optional fields are license, compatibility, metadata, and the experimental allowed-tools. Anthropic's platform adds two rules: no XML tags in either required field, and no anthropic or claude in a name. Because the name must match the folder, a website skill called scan-url naturally lives at /.well-known/agent-skills/scan-url/SKILL.md.

How do agents discover skills on a website?

They fetch one JSON file at a well-known path. The mechanism is the Agent Skills Discovery draft in Cloudflare's GitHub repository: publishers MUST provide /.well-known/agent-skills/index.json, listing every skill (cloudflare/agent-skills-discovery-rfc). As of 2 October 2026 it is version 0.2.0, published 17 January 2026 and last updated 12 March 2026. It builds on RFC 8615 but is not an IETF document, and I found no agent-skills entry in the IANA well-known URI registry, last updated 16 September 2026. Expect details to change.

Here is aiSiteReady's real index, fetched from production on 2 October 2026.

{
  "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
  "skills": [
    {
      "name": "scan-url",
      "type": "skill-md",
      "description": "Submit a public URL and receive an Agent Readiness Score (0–100) with per-check evidence and prioritized, localized remediation guidance for AI-agent and AI-search readiness.",
      "url": "https://www.ai-siteready.com/.well-known/agent-skills/scan-url/SKILL.md",
      "digest": "sha256:1d7a33bcc768060bbba4328bec09637e6dcc043ada3c85f19f758879bde54ffb"
    }
  ]
}

$schema is an opaque version identifier. Clients should warn on one they don't recognize and skip the index, and treat an absent one as the old 0.1.0 format. Every entry in skills needs all five fields.

Entry fieldRule from the draft
nameSame rules as the SKILL.md name
typeskill-md for one file, or archive for a .tar.gz or .zip with SKILL.md at its root; clients skip unknown types
descriptionMax 1,024 characters; SHOULD match the front-matter description
urlAbsolute, path-absolute, or relative to the index URL
digestsha256: plus 64 lowercase hex characters of the artifact's raw bytes

Compare the $schema string; don't fetch it. The draft says it need not resolve, and the host schemas.agentskills.io didn't resolve in DNS when I checked on 2 October 2026.

Four-step flow: fetch index.json, read the scan-url entry, download SKILL.md and check its sha256 digest, then follow the skill's instructions.

Who actually reads the index? Support is uneven. The open-source skills installer from Vercel Labs implements version 0.2.0: npx skills add https://your-domain reads the index, downloads every listed skill, checks each hash against digest, and then lets you pick which to install (source, Evil Martians' August 2026 write-up). Elsewhere, support is thin: a May 2026 request for Claude Code to load skills from a well-known URL was closed as inactive a month later, unimplemented.

How do you write a SKILL.md that agents actually use?

Start with the description: it is all the agent sees before it decides. Anthropic's guide says to state what the skill does and when to use it, in the third person, with specific key terms, and warns against vague ones like "Helps with documents" (Skill authoring best practices).

For a website, name the user's situation: "Looks up the status of an order by order number. Use when a user asks where an order is, whether it shipped, or when it will arrive."

The body then needs five sections, in this order.

SectionWhat goes in itWhy
When to useSituations in the user's own wordsConfirms the match
How to invokeExact endpoints, methods, parameters, a sample requestThe agent can't guess your API
OutputThe shape of the resultStops invented fields
Errors and limitsEach failure, the next step, rate limitsPrevents retry loops
Scope and safetyWhat the skill won't do; no secretsThe text goes straight into agent context

Annotated SKILL.md: name and description in the front-matter, then five body sections, from When to use to Scope and safety.

The last row matters more than it looks. A SKILL.md goes directly into an agent's context, so the draft warns that a malicious one can inject instructions and recommends that clients load skills only from allowlisted origins (draft, security considerations).

Write only about your own site's actions, never ask an agent to send user data anywhere, and say what is out of scope. The draft also says clients SHALL NOT execute files under scripts/ by default, so a website skill shouldn't depend on scripts. Use a single skill-md file under 500 lines (agentskills.io).

Here are the front-matter and the first section of aiSiteReady's own scan-url skill, as served, with the intro paragraph cut.

---
name: scan-url
description: Scan a public http(s) URL for AI-agent and AI-search readiness and return an Agent Readiness Score (0-100) with per-check evidence and prioritized, localized remediation guidance across discoverability, content accessibility, bot governance, protocol/API discovery, and commerce.
license: Apache-2.0
---

# Agent Readiness Scan

## When to use

- A user asks whether a site is discoverable, parseable, or usable by AI agents and
  AI-search crawlers.
- You need a structured readiness report (robots/sitemap/llms.txt, structured data,
  bot governance, MCP/A2A/API discovery, commerce signals) for a URL.

Reading it against my own checklist, I found three gaps:

  1. The description says what the skill does but has no "use when" clause; that sentence sits in the body, which the agent reads only after deciding.
  2. The REST option says to "poll the returned report path", but the 202 from POST /api/v1/scans carries an id and an access_token, not a path, and a non-public report can't be read without the token.
  3. There is no errors section.

This is what I would add, using the status codes from the live OpenAPI document.

### Option B - REST API

1. `POST /api/v1/scans` with {"url": "https://example.com", "profile": "all", "locale": "en"}.
   The `202` response carries `id` and `access_token`.
2. Poll `GET /api/v1/scans/{id}` with the header `X-Scan-Access-Token: <access_token>`
   until `status` is `succeeded` or `failed`.
3. Read `GET /api/v1/scans/{id}/report.json` with the same header.

## Errors and limits

- `400` `invalid_scan_url`: the URL is not a public http(s) address. Ask for another one.
- `409` `idempotency_conflict`: the same `Idempotency-Key` was sent with a different body.
- `422` `validation_error` or `invalid_check_selection`: the request body is invalid. Fix it; do not resend it unchanged.
- `429` `rate_limited`: wait the number of seconds in `Retry-After`, then retry once.
- `503` `queue_unavailable`: try again later; do not loop.
- Only public pages are scanned: no logins, no remote JavaScript.

If you describe limits in the skill, publish them as headers too: see RateLimit-Policy headers for AI agents.

How should you serve the index and the SKILL.md?

As static, public, cacheable files with honest media types. The draft is specific about what servers MUST and SHOULD do (draft, HTTP considerations); the last column is what aiSiteReady's production routes returned on 2 October 2026.

ResourceRequirementaiSiteReady
Index media typeMUST be application/jsonapplication/json; charset=utf-8
SKILL.md media typeMUST be text/markdown or text/plaintext/markdown; charset=utf-8
MethodsMUST support GET and HEADGET only on 2 October 2026; both since the fix (see below)
Missing skillMUST return 404404 with a JSON body
CachingSHOULD set Cache-Controlpublic, max-age=3600
CORSSHOULD set headers for browser consumersNot set
HTTP/2 200
content-type: application/json; charset=utf-8
cache-control: public, max-age=3600

HTTP/2 200
content-type: text/markdown; charset=utf-8
cache-control: public, max-age=3600
vary: Accept-Encoding

The url field can be absolute, path-absolute (resolved against the index origin), or relative to the index directory. aiSiteReady publishes absolute URLs, and that has a cost.

The builder takes the origin from NUXT_PUBLIC_SITE_URL; when it's unset, it falls back to the dev origin localhost:5173. The scanner rejects that URL, because localhost has no dot and 5173 is not port 80 or 443, so a production deploy that forgot the variable publishes an index the check rates PARTIAL. Path-absolute URLs would avoid the problem, and the installer I read resolves them with new URL(entry.url, indexUrl), though you should check every client you care about.

For a static site you don't need a framework. This script copies each skill byte for byte into the public folder and writes the index from the same bytes.

// node build-skills-index.mjs https://example.com
// Reads skills/<name>/SKILL.md, copies each file byte for byte into public/,
// and writes an index whose digests hash those same bytes.
import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";

const origin = (process.argv[2] ?? "").replace(/\/$/, "");
if (!origin.startsWith("https://")) throw new Error("usage: node build-skills-index.mjs https://example.com");

const out = "public/.well-known/agent-skills";
const skills = [];
const names = readdirSync("skills", { withFileTypes: true })
  .filter((d) => d.isDirectory()) // skip stray files such as .DS_Store
  .map((d) => d.name)
  .sort();
for (const name of names) {
  const bytes = readFileSync(join("skills", name, "SKILL.md")); // raw bytes, never re-encoded
  const front = /^\uFEFF?---\r?\n([\s\S]*?)\r?\n---/.exec(bytes.toString("utf8"))?.[1] ?? "";
  const field = (key) =>
    new RegExp(`^${key}:\\s*(.+)$`, "m").exec(front)?.[1].trim().replace(/^(["'])(.*)\1$/, "$2");
  if (field("name") !== name) throw new Error(`${name}: front-matter name must equal the folder name`);
  const description = field("description");
  if (!description || /^[>|]/.test(description)) throw new Error(`${name}: front-matter needs a one-line description`);

  mkdirSync(join(out, name), { recursive: true });
  writeFileSync(join(out, name, "SKILL.md"), bytes); // serve exactly what was hashed
  skills.push({
    name,
    type: "skill-md",
    description, // same text as the front-matter: the draft says it SHOULD match
    url: `${origin}/.well-known/agent-skills/${name}/SKILL.md`,
    digest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
  });
}

const schema = "https://schemas.agentskills.io/discovery/0.2.0/schema.json";
writeFileSync(join(out, "index.json"), `${JSON.stringify({ $schema: schema, skills }, null, 2)}\n`);
console.log(`wrote ${skills.length} skill(s) to ${out}`);

Check the content-type your host sends for .md files. Stock nginx has no MIME type for md and sends application/octet-stream, which breaks the draft's MUST; add types { text/markdown md; } to the http block (in a server block it replaces the whole map, JSON included). Also make sure a missing skill returns 404: a single-page app that answers every unknown path with its HTML shell and a 200 will happily "serve" a missing SKILL.md.

How do you keep the digest in sync with the served file?

Compute it from the same bytes you serve, in the same step. The digest is sha256: plus the hash of the artifact's raw bytes, and clients MUST verify it after download and MUST NOT use content that fails (draft, integrity). The Vercel Labs installer does exactly that: fetchArtifactSkillByEntry hashes the downloaded bytes and returns null if the result differs from entry.digest, and fetchAllSkills filters the nulls out. The skill silently disappears from the list; no digest error is ever shown.

So drift is silent: edit the SKILL.md, forget to rebuild the index, and conformant clients stop using your skill with no error you will ever see. Hash the decoded body, not a gzip transfer. Anything that rewrites bytes after you hash them, such as a line-ending conversion or a proxy that injects a footer, breaks the match too.

aiSiteReady avoids the problem structurally. The skill text lives in one TypeScript constant. The route returns that constant, and the digest is computed from it when the module loads. There is no second copy to edit separately.

// the skill constant and its digest (abridged)
import { createHash } from "node:crypto";

export const SCAN_URL_SKILL_MD = `---
name: scan-url
description: Scan a public http(s) URL …
---
…`;

export const SCAN_URL_SKILL_DIGEST = `sha256:${createHash("sha256")
  .update(SCAN_URL_SKILL_MD, "utf8")
  .digest("hex")}`;
// the SKILL.md route handler (abridged)
export default defineReadOnlyHandler((event) => { // GET and HEAD alike; 405 for other methods
  setResponseHeader(event, "Content-Type", "text/markdown; charset=utf-8");
  return SCAN_URL_SKILL_MD; // the exact constant the digest was computed from
});

I checked it from the outside on 2 October 2026. The production file is 2,476 bytes, and shasum -a 256 prints 1d7a33bc…bde54ffb, the digest in the index. The skill body is also origin-relative, with paths like /api/mcp/ rather than absolute URLs, so the bytes, and the digest, are identical on every deploy domain.

Digest check: the 2,476-byte SKILL.md matches the index digest and loads; after a one-line edit the hash differs and the client rejects it.

Hosting skills somewhere you don't control, such as raw GitHub URLs, loses this guarantee: upstream pushes change the bytes under a digest you already published. Evil Martians' rule is to author anywhere but publish from a place you control (Evil Martians).

How do you verify your Agent Skills index?

Check three things: the index parses and has the right shape, every url serves the right media type, and every digest matches the bytes. Three curl commands cover the manual version.

# 1. headers and body of the index
curl -sSi https://example.com/.well-known/agent-skills/index.json | head -20
# 2. every skill and its advertised digest
curl -sS https://example.com/.well-known/agent-skills/index.json | jq -r '.skills[] | [.name, .digest] | @tsv'
# 3. the real hash of a served skill, to compare by eye
curl -sS https://example.com/.well-known/agent-skills/scan-url/SKILL.md | shasum -a 256

For CI, this script does all of it. It needs curl, jq, and shasum (on Linux without it, swap in sha256sum) and exits non-zero on any failure.

#!/usr/bin/env bash
# usage: ./check-skills.sh https://example.com
set -euo pipefail
ORIGIN="${1:?usage: check-skills.sh https://example.com}"
ORIGIN="${ORIGIN%/}"
INDEX="$ORIGIN/.well-known/agent-skills/index.json"
SCHEMA="https://schemas.agentskills.io/discovery/0.2.0/schema.json"
bad=0
ok()   { printf 'ok    %s\n' "$*"; }
fail() { printf 'FAIL  %s\n' "$*"; bad=1; }
tmp="$(mktemp)"; trap 'rm -f "$tmp"' EXIT

# 1. The index: status, media type, HEAD support, $schema
meta="$(curl -sS -o "$tmp" -w '%{http_code} %{content_type}' "$INDEX")"
[[ "$meta" == 200\ application/json* ]] && ok "index: GET 200, ${meta#* }" || fail "index: GET -> $meta"
[[ "$(curl -sS -I -o /dev/null -w '%{http_code}' "$INDEX")" == 200 ]] \
  && ok "index: HEAD 200" || fail "index: HEAD is not 200 (the draft says servers MUST support HEAD)"
body="$(cat "$tmp")"
jq -e --arg s "$SCHEMA" '."$schema" == $s' <<<"$body" >/dev/null \
  && ok "index: \$schema is the 0.2.0 URI" || fail "index: missing or unexpected \$schema"
jq -e '.skills | type == "array" and length > 0' <<<"$body" >/dev/null 2>&1 \
  && ok "index: skills[] has entries" || fail "index: skills[] is missing or empty"

# 2. Every entry: fields, URL, media type, front-matter name, and the digest
while read -r entry; do
  name="$(jq -r '.name // ""' <<<"$entry")"; url="$(jq -r '.url // ""' <<<"$entry")"
  want="$(jq -r '.digest // ""' <<<"$entry")"
  type="$(jq -r '.type // ""' <<<"$entry")"
  jq -e '.description | type == "string" and length > 0 and length <= 1024' <<<"$entry" >/dev/null \
    || fail "$name: description is missing or longer than 1,024 characters"
  [[ "$type" == skill-md ]] || { fail "$name: type is '$type'; this script verifies skill-md entries only"; continue; }
  [[ "$name" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ && ${#name} -le 64 ]] || fail "$name: invalid name"
  [[ "$want" =~ ^sha256:[0-9a-f]{64}$ ]] || fail "$name: digest is not sha256:<64 lowercase hex>"
  case "$url" in https://*|http://*) ;; /*) url="$ORIGIN$url" ;; *) url="$ORIGIN/.well-known/agent-skills/$url" ;; esac
  [[ "$url" == https://* && "$url" != *localhost* ]] || { fail "$name: url is not a public https URL ($url)"; continue; }

  meta="$(curl -sS -o "$tmp" -w '%{http_code} %{content_type}' "$url")"
  [[ "$meta" == 200\ text/markdown* || "$meta" == 200\ text/plain* ]] \
    && ok "$name: GET 200, ${meta#* }" || fail "$name: GET -> $meta"
  got="sha256:$(shasum -a 256 "$tmp" | cut -d' ' -f1)"
  [[ "$got" == "$want" ]] && ok "$name: digest matches the served bytes" \
    || fail "$name: digest drift - index says $want, served bytes hash to $got"
  head -n 10 "$tmp" | tr -d '\r' | grep -qx "name: $name" \
    && ok "$name: front-matter name matches" || fail "$name: front-matter name differs from the index"
done < <(jq -c '.skills[]?' <<<"$body" 2>/dev/null)

exit "$bad"

Run against aiSiteReady's production site on 2 October 2026, it printed this.

ok    index: GET 200, application/json; charset=utf-8
FAIL  index: HEAD is not 200 (the draft says servers MUST support HEAD)
ok    index: $schema is the 0.2.0 URI
ok    index: skills[] has entries
ok    scan-url: GET 200, text/markdown; charset=utf-8
ok    scan-url: digest matches the served bytes
ok    scan-url: front-matter name matches

That failure was real, and it was ours. On 2 October 2026 our well-known routes were GET-only handlers, and a HEAD request to them returned 404, while the draft says servers MUST support both methods. A client that probed with HEAD before downloading would have misjudged our site. The fix was small: the route files lost their .get suffix, and one handler now answers GET and HEAD with the same status and headers, so that line reads ok index: HEAD 200. I only found the bug because I wrote this script, so run yours before you trust your own setup.

What mistakes break an Agent Skills index?

Digest drift, a vague description, and a localhost url cause most broken indexes, and almost all of these failures are silent: a client skips the skill, or a scanner downgrades the check. These are the ones I'd test for first.

MistakeWhat happensFix
Digest driftConformant clients refuse the skill; you see nothingDerive the digest from the served bytes
Vague descriptionThe agent never picks the skillSay what it does and "Use when"
A skill that needs a loginThe agent stalls at step oneDocument how to get access, or don't publish it
localhost or a dev port in urlOur check rejects the URL (no dotted hostname, or a port other than 80/443) and rates the index PARTIAL; a staging hostname passes the check but sends agents to stagingSet the public origin, or use path-absolute URLs
200 HTML shell for a missing fileA client that skips verification loads HTML as instructionsReturn 404
Missing or invented $schemaThe installer I read treats a missing one as legacy 0.1.0 and skips an unknown one; our check is PARTIAL without one but accepts any non-empty string, even an invented oneCopy the 0.2.0 URI exactly
Old 0.1.0 shape or the /.well-known/skills/ pathOur check probes only the new pathMove to agent-skills/ with type, url, digest
Index description differs from front-matterBreaks a SHOULD; agents see two storiesGenerate both from one source

The last row is another confession. Our index reuses the description from our A2A Agent Card, while the SKILL.md front-matter carries a different, longer sentence. Both are accurate, but the draft says they SHOULD match. The build script above avoids the problem by reading the description from the front-matter.

How does aiSiteReady check an Agent Skills index?

aiSiteReady's agent_skills check makes one request to /.well-known/agent-skills/index.json and validates it against Agent Skills Discovery 0.2.0: the shape, the five fields, and the digest format, but not the served SKILL.md. Your script covers the rest. It sits in the Protocol/API category beside the MCP Server Card and API catalog checks, and the 30-minute audit shows where it fits in a full pass.

StatusCondition
PASS$schema declared, JSON content type, and at least one entry with valid name, type, description, url, and digest
PARTIALValid entries but no $schema, or a parseable index served with a non-JSON type such as text/plain; entries exist but none is valid; skills is missing or empty; or the JSON doesn't parse
UNKNOWNThe index couldn't be fetched safely; a 403 or 429 counts as a block, not a verdict
FAILNo index: a 404 or any other error status except 403/429, or an HTML page (an SPA shell, or JSON sent as text/html) instead of JSON

Four limits to know:

  • The check makes one request and never downloads a SKILL.md, so it validates the digest's format but can't catch drift; that's what your script is for.
  • It probes only the well-known path, with no Link header discovery.
  • The Protocol/API category is off in the Content site profile, so it counts only in the default All checks profile and the API/Application and E-commerce profiles.
  • Entry URLs must be absolute http(s) URLs, or resolve to one against the index URL, with a dotted hostname on port 80 or 443. The check never resolves or fetches them, so http://localhost:5173/… fails while a staging hostname or a private IP passes.

The methodology page shows how much the Protocol/API category (listed there as Protocols) weighs in each profile.

Run a free scan to see whether an agent finds your skills index, which entries pass, and what to fix first, in English, Ukrainian, or Russian. For the full set of layers, start from what AI agent readiness means.

The short version: a skill is a SKILL.md that says when to use it and exactly what to call, the index is five fields per entry, and the digest must be computed from the bytes you serve. Write the description as a trigger, generate the index from the files, and run the verification script on every release.

IMozz has 20 years in software development, with the past year spent building with LLMs. He builds aiSiteReady, a read-only scanner that checks whether AI agents can read and call a site. It server-renders its own content as a working example.

Frequently asked questions

Is /.well-known/agent-skills/index.json an official standard?
Not yet. It comes from the Agent Skills Discovery draft, version 0.2.0, published in a Cloudflare GitHub repository and last updated in March 2026. It builds on RFC 8615 well-known URIs, but it is not an IETF RFC, and I found no agent-skills entry in the IANA well-known registry. The SKILL.md format itself is a separate open standard at agentskills.io.
What must the SKILL.md front-matter contain?
Two fields: name and description. The name is at most 64 characters of lowercase letters, digits, and hyphens, with no leading, trailing, or double hyphen, and it must match the folder name. The description is 1 to 1,024 characters and should say both what the skill does and when to use it, because agents match tasks against it before loading anything else.
How is the digest in the skills index calculated?
It is the SHA-256 hash of the artifact's raw bytes, written as sha256: followed by 64 lowercase hex characters. For a skill-md entry that means the exact SKILL.md file the url serves. Clients must verify it after download and must not use content that fails. On the command line, curl the url and pipe the body to shasum -a 256.
Do AI agents actually read my skills index today?
Some do, but support is uneven. The open-source skills installer from Vercel Labs reads the index and verifies each digest, and agentskills.io lists more than forty products that support the SKILL.md format. Many agents never fetch a well-known index, so publish it as a cheap discovery layer, not a traffic source, and make the SKILL.md useful on its own.