# Ship agent skills like packages: discovery index, digests, and install sources

> How to publish agent skills so any AI coding agent can find and install them: the .well-known/agent-skills/index.json discovery index, SHA-256 integrity digests, single-file vs multi-file vs bundle packaging, and every install command. Worked from the catalog we just open-sourced.

- Date: 2026-08-12T00:00:00.000Z
- Authors: Rita Klubochkina, Travis Turner
- Categories: AI, DX
- URL: https://evilmartians.com/chronicles/publishing-agent-skills-discovery-index

---

Two posts ago, an AI startup found us because Claude, which recommended [Evil Martians](/) when they asked for a senior dev agency. One post ago, we measured the traffic behind that. Over two months, **coding agents read [evilmartians.com](/) more than twice as often as people did**: 268,000 agent requests against 107,000 human pageviews. Both pointed to the same idea: site audiences are increasingly machines, so build for them. This post takes the next step. We stopped waiting for agents to read our pages and started handing them something to install: our own engineering practice, packaged as agent skills, published at [/agent-skills](/agent-skills), and discoverable through a `.well-known` index any agent can find. Here's how to publish yours.

An [agent skill](https://agentskills.io/) is a folder of instructions that teaches an AI coding agent a repeatable practice: the conventions and checks for doing a specific job well, packaged so any agent can follow them. Someone authors a practice once, then everyone else's agents install it. This post is a full how-to for publishing your own skills and making them discoverable, using our catalog as the worked example.

---

*Let’s run a 2-week LLM-optimization sprint with Evil Martians to improve your discoverability.* [Contact Evil Martians](https://evilmartians.com/contact-us)

---

**TL;DR:** An agent skill is a `SKILL.md` file (plus optional extra files) that teaches a coding agent a repeatable practice. To make yours discoverable, publish a `.well-known/agent-skills/index.json` at your domain: a short list of `name`, `description`, `type`, `url`, and `sha256:` digest per skill. 

The [`npx skills` installer](https://github.com/vercel-labs/skills) reads it. Author each skill in a GitHub repo (installers and Claude's plugin marketplace resolve straight from the repo), but re-host the bytes on your own domain so the digest is computed over what *you* serve. Skills come in three shapes: single-file, multi-file, and bundle, each handled differently. Offer more than one install command, because your readers live in different tools. 

None of this is new machinery: it's what npm and RubyGems have always done, pointed at a folder with a `SKILL.md` in it.

The playbook below is also packaged as a skill, the same way we did for [part one](/chronicles/how-to-make-your-website-visible-to-llms). Install it and your agent can publish your catalog for you, discovery index and digests included. Read on for what it does, and why each piece works the way it does.

*Download Evil Martians' agent skill*: https://github.com/evilmartians/agent-skills

## What's an agent skill, and why open-source ours?

An agent skill is the smallest useful unit of "how we do this." 

Anthropic [introduced the format](https://claude.com/blog/skills) for Claude in October 2025 and released it as an [open standard](https://agentskills.io/) that December. [Cursor](https://cursor.com/), [Codex](https://openai.com/codex/), [GitHub Copilot](https://github.com/features/copilot), and [Gemini CLI](https://geminicli.com/) read the same file now, each from a skills directory it scans on startup. 

A skill is a `SKILL.md` file: YAML front matter with a `name` and a `description` that tells the agent *when* to reach for it, then Markdown instructions for the *how*. Some skills ship extra files alongside: a script, a reference doc, a template. That's the entire format; so, no runtime, API, or server. An agent loads the file when the description matches the task, and follows it.

We use a lot of these internally, so we published the ones useful beyond our own projects at [/agent-skills](/agent-skills). Here's a few of them:

- **`good-readme`** — writes a README the way we'd review it, with the sections that actually get read.
- **`layered-rails`** — our conventions for structuring a Rails app in layers, so the code stays legible as it grows.
- **`secure-npm-package`** — the checks we run before publishing an npm package, so a supply-chain mistake gets caught before it ships.
- **`storybook-workbench`** — eleven skills for building and auditing a [Storybook](https://storybook.js.org/), published as one bundle.
- **`inertia-rails-skills`** — another eleven, covering [Inertia.js](https://inertiajs.com/) on Rails across React, Vue 3, and Svelte, published from the [Inertia Rails](https://github.com/inertia-rails/skills) project's own repo.
- **`llms-visibility`** — makes a site readable to LLMs and the coding agents that fetch URLs: Markdown routes, `Accept` content negotiation, and `llms.txt`, plus the AI SEO anti-patterns to refuse. It's [part one](/chronicles/how-to-make-your-website-visible-to-llms) of this series, packaged as a skill.

Why give them away? The same reason we open-source everything else! When a client's coding agent installs `layered-rails`, it structures their Rails app the way our engineers would, on a Tuesday afternoon, with no Martian in the room. Sharing a skill is the most compact form of distribution we've found for an opinion: it travels into codebases we'll never touch and does the work there. It's the same approach that turned [PostCSS](https://postcss.org/) and [imgproxy](https://imgproxy.net) from things we built for ourselves into tools other teams now run in production. 

That said, this only works if agents can *find* the skills. Which brings us to the actual subject of this post.

## The discovery index: one JSON file agents already look for

Here's the mechanism. The [`npx skills`](https://github.com/vercel-labs/skills) installer (an open source tool from Vercel Labs, distributed on npm as `skills` and backed by the [skills.sh](https://skills.sh) registry) can install a skill straight from a hosted site, not only from a Git repo. Just point it at our skills page:

```
npx skills add https://evilmartians.com/agent-skills
```

Behind that command, it reads the discovery index we serve at `https://evilmartians.com/.well-known/agent-skills/index.json` and lists every skill we publish for you to pick from in an interactive prompt. 

This is the same idea as `llms.txt` from part one, `robots.txt`, or a sitemap: a well-known path where machines look for a machine-readable summary of what you offer. To skip the prompt, name what you want up front: `--skill <name>` installs one, `--skill '*'` takes the whole catalog. The full `https://` URL is what triggers discovery: hand `npx skills` a bare `owner/repo` or domain instead and it treats the source as a Git repo to clone, not a site to read an index from.

*Read also Evil Martians' article*: https://evilmartians.com/chronicles/how-to-make-your-website-visible-to-llms

The file is small, and let's note that the format isn't ours. The `.well-known/agent-skills/index.json` discovery document comes from an open [RFC published by Cloudflare](https://github.com/cloudflare/agent-skills-discovery-rfc). It builds on [RFC 8615](https://www.rfc-editor.org/rfc/rfc8615), the `.well-known/` convention that already gives us `robots.txt` and `security.txt`, and it points at the [`schemas.agentskills.io/discovery/0.2.0`](https://agentskills.io/) schema. 

The document is a list of entries:

```json
{
  "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
  "skills": [
    {
      "name": "llms-visibility",
      "description": "Make a site readable to ChatGPT, Claude, Perplexity, and the coding agents that fetch URLs…",
      "type": "skill-md",
      "url": "https://evilmartians.com/agent-skills/llms-visibility/SKILL.md",
      "digest": "sha256:9f2b…"
    },
    {
      "name": "layered-rails",
      "description": "Write, refactor, and review Rails code using layered architecture principles…",
      "type": "archive",
      "url": "https://evilmartians.com/agent-skills/layered-rails.tar.gz",
      "digest": "sha256:1c4a…"
    }
  ]
}
```

We have five fields per skill. `name` is the slug an installer uses. `description` is the same one-line "use this when…" the agent reads to decide relevance. `type` is either `skill-md` (the payload is a lone `SKILL.md`) or `archive` (it's a tarball with more than one file). `url` points at the bytes. `digest` is a SHA-256 hash of exactly those bytes, in `sha256:<hex>` form. 

The installer re-hashes whatever it downloads and refuses to install on a mismatch, and the RFC makes that mandatory rather than optional politeness: a conformant client MUST verify the digest and MUST NOT use content that fails. So the digest is a tamper check: it guarantees you got the skill the publisher meant to publish, not something rewritten in transit.

We have two top-level keys, and that's the whole document. There's no `version`, `origin`, `bundles` array, and no per-skill `bundle` field; a bundle is represented only as its member skills, each a normal entry. 

Inventing a structure the schema doesn't define is the most common way to hand an installer something it ignores. 

The one field with rules of its own is `name`, which has to be a valid skill name (lowercase letters, digits, single dashes) or an installer won't take the entry. Our build checks that before writing the index and leaves a skill out with a warning rather than publishing an entry nothing can install.

Who reads this format today? The `npx skills` installer is the reference consumer, but the RFC is vendor-neutral by design, so the convention isn't owned by one client. Cloudflare authored the discovery spec, Anthropic defined the underlying skill format, and the schema lives at agentskills.io. 

Any agent, registry, or installer can fetch the index and resolve a skill from it, the same way any crawler can read your `robots.txt`. The schema is versioned (we're on `0.2.0`), so it can evolve without breaking the installers pinned to a given version.

If part one's lesson was "_ship clean Markdown at a well-known URL and tell the world it exists_," this is the same move applied to a different artifact. If you have skills, put a list of them where agents look.

## Single-file, multi-file, and bundle: three shapes, three install paths

That `type` field is doing more work than a two-value enum suggests. Not every skill is shaped the same, and the shape decides how it's served and installed. There are three to look at:

| Shape | `type` in the index | What `url` points at | What the installer does |
| --- | --- | --- | --- |
| Single-file | `skill-md` | the `SKILL.md` itself | copies one file |
| Multi-file | `archive` | a flat `<name>.tar.gz` | unpacks it into the skill's folder |
| Bundle | one entry per member skill | each member's own payload | installs members one at a time, or takes the combined `<bundle>-bundle.tar.gz` in one shot |

**A single-file skill is just `SKILL.md`.** Nothing else in the folder. In the index, it is `type: skill-md`, and its `url` points straight at the Markdown file. This is the least work to install, because installing is copying one file to the right place. It's also the only shape short enough to paste directly into a running agent, so for these we offer a "_copy the whole `SKILL.md`_" action alongside the install command.

**A multi-file skill ships `SKILL.md` plus extra files**—a script it runs, a reference table, a template it fills in. You can't install that by copying one file, so we pack it into a flat `<name>.tar.gz` (`SKILL.md` and its friends at the archive root) and the index marks it `type: archive`, with the `url` pointing at the tarball. The installer unpacks the archive into the skill's folder, and the digest covers the whole tarball.

One thing to get right while you're building that tarball: the archive and its hash have to be made together. A `.tar.gz` bakes in more than file contents—member order, file modes, embedded mtimes, and gzip's own timestamp all end up in the bytes—so the same skill archived twice can hash differently while nothing about it changed. We rebuild the archives and the index in one pass, so ours can't drift apart. 

If yours are built in separate steps, pin the variance (`tar --sort=name --owner=0 --group=0 --numeric-owner --mtime='UTC 2020-01-01'`, piped through `gzip -n` to drop the timestamp) or you'll advertise a digest for a file you no longer serve.

**A bundle is one repo that publishes several skills at once.** Our `storybook-workbench` is one repo carrying eleven skills. Each of the eleven gets its *own* discovery entry, under its own name, so `npx skills … --skill <one-of-them>` installs exactly one. The bundle's own page has no `SKILL.md` to preview, because a bundle is a container for skills rather than a skill itself. What it *does* offer is a combined `<bundle>-bundle.tar.gz` with every skill side by side as its own folder, so a single `curl … | tar -xz -C ~/.claude/skills` drops the whole set into place at once. That's usually the fastest way to adopt a bundle: one command, eleven skills.

This specificity matters when you publish your own. A lone `SKILL.md` and a folder of files are handled differently at every layer: the archive we build, the `type` in the index, the digest target, and the install command a reader copies. If you get the shape right, the rest follows. But if you treat a multi-file skill like a single file, the install drops everything but `SKILL.md`.

## Author in a repo, serve from your own domain

Every skill we publish is authored in a GitHub repo and served to installers from `evilmartians.com`. Those are two different jobs, and it pays to be deliberate about which one does what. The repo is non-negotiable, for three reasons:

- **The authoring workflow lives there** — a skill is code-adjacent, it gets reviewed, versioned, and improved like anything else, and Git is where that happens.
- **`gh skill install` reads `SKILL.md` straight from the repo** — a GitHub-native install path needs a GitHub-native home.
- **Claude's plugin marketplace is repo-shaped** — a repo with a [`.claude-plugin/marketplace.json`](https://docs.claude.com/en/docs/claude-code/plugin-marketplaces) at its root is an installable marketplace, and the `name` in that manifest is the `@marketplace` half of `claude plugin install <skill>@<marketplace>`. Without a repo there's no manifest, and without a manifest there's no plugin to install.

But look back at the `url` fields in that index: they point at `evilmartians.com`, not `raw.githubusercontent.com`. The RFC allows either—a `url` resolves against the index and can live at any origin—so this is a choice, and we make it for three reasons:

- **Half the catalog has no raw URL to point at** — that flat `<name>.tar.gz` every multi-file skill installs from doesn't exist in the repo; our build creates it. GitHub will hand you a tarball of an entire repo, wrapped in a prefix folder with everything else inside, which is not the file an installer needs.
- **A payload on your own domain is a request you can see** — classified at your own edge, which is the only reason we can measure any of this.
- **The digest is computed over the exact bytes we serve** — the file our build just wrote into `public/`. The hash can't disagree with what we serve, because it's taken over what we serve.

That final one has a payoff worth elaborating upon. 

A raw URL tracks a branch perfectly well: point it at `main` and it always serves the latest. What it can't do is keep the digest true while it does. Publish a hash, let upstream push, and the bytes change underneath a digest you already advertised. Verification is a MUST, so every conformant installer now refuses the skill, and keeps refusing until you happen to rebuild. 

With raw URLs you pick your poison: stale-but-valid (pin a commit SHA) or fresh-but-broken (track the branch and hope). Re-hosting removes the choice, because the fetch, the write, and the hash all happen in one build, so **an upstream push goes live with a correct hash, automatically.**

The bill for that comes due as latency. A raw URL is live the instant someone merges; ours is live on the next deploy, so a skill author pushing a fix waits for a site build to see it in the index. We think that's a fair trade for a digest that's never wrong, but it's a real cost, and it's the reason to keep the vendoring step boring enough that a rebuild is cheap.

Be clear about what re-hosting does and doesn't buy, too. It guarantees *consistency*, meaning the bytes always match the published digest. It doesn't buy immunity from upstream. Push something bad to a skill's repo and we'll re-host it and hash it just as faithfully. The digest protects the trip from us to the installer; reviewing what goes into the repo is a separate job, and still yours.

In general, as a rule, **serve the artifact and compute its integrity digest from the same place.** Author wherever you like, but publish from a location you control.

## Every install method, and why we shipped all of them

Open any skill's card on [/agent-skills](/agent-skills) and you'll see a target selector (Claude Code, Cursor, Codex, GitHub Copilot, Gemini CLI) over a set of install methods. `SKILL.md` is a cross-agent standard, so one payload serves every agent, and only the destination directory differs. There are four:

- **[`npx skills`](https://github.com/vercel-labs/skills)** — reads our discovery index and resolves the payload itself: `npx skills add https://evilmartians.com/agent-skills --skill good-readme -a claude-code -g`. It's the only method that exercises the whole chain: it finds the index, picks the `url` matching the skill's `type`, and verifies the digest before anything lands on disk.
- **[`claude plugin`](https://docs.claude.com/en/docs/claude-code/plugins)** — Claude's marketplace path: `claude plugin marketplace add evilmartians/agent-skills`, then `claude plugin install good-readme@evilmartians`. That `@` half is the `name` from the repo's `.claude-plugin/marketplace.json`, not the repo's own name, so read it rather than guess it. Claude-only, and the *best* option for a bundle, because one plugin install pulls the whole set.
- **[`gh skill`](https://cli.github.com/)** — installs one skill straight from the repo that authors it, no discovery index involved: `gh skill install evilmartians/agent-skills good-readme`. It finds a skill by its `SKILL.md`, so bundles are the one shape we don't offer it for. It's also the only path that doesn't ask you to trust us: it pulls from the repo you can sit and read, not from our domain and our digest.
- **`curl`** — a self-contained one-liner that pulls the payload from our domain and drops it into the agent's skills directory. No installer to trust, nothing to learn: it's `curl` and `tar`.

Four commands for one file might seem redundant until you see what each does that the others can't. 

`claude plugin` installs an eleven-skill bundle in a single step. `gh skill` skips us entirely and installs from the repo you just read. `npx skills` is the only one that verifies the digest before anything lands on disk. `curl` needs nothing installed first, which is the whole point inside a container build where adding a dependency is the cost you're avoiding.

Covering all four costs us a few generated command strings. The alternative costs us readers since someone who can't run your one command rarely goes hunting for a workaround; they just close the tab. We'd rather ship four than lose them.

## Four checks before you ship yours

Everything above is easy to get subtly wrong in ways that only crop up on someone else's laptop, so verify it the way an installer will:

- **The index parses and lists what you think it lists.** `curl -fsSL https://yourdomain.com/.well-known/agent-skills/index.json` should return valid JSON with every skill you meant to publish, and nothing you didn't.
- **One digest matches its payload.** Pick an entry, run `curl -fsSL <its url> | sha256sum`, and compare. That's the check a conformant installer runs before it writes anything to disk, and it's the one that breaks the moment you hash something other than what you serve.
- **The archives are flat.** `tar -tzf <name>.tar.gz` should list `SKILL.md` at the root, not `<name>/SKILL.md`. A wrapping folder is the classic broken install: the files land one level too deep and the agent never sees the skill.
- **A real install works end to end.** `npx skills add https://yourdomain.com/agent-skills --skill <name>` should drop the skill into the target agent's skills directory. Ask the agent something the skill covers, and watch whether it picks it up: that's the only test of whether your `description` is doing its job.

## Measuring what gets installed

*Read also Evil Martians' article*: https://evilmartians.com/chronicles/which-ai-actually-reads-your-site-two-months-of-llm-traffic-measured

[Part two](/chronicles/which-ai-actually-reads-your-site-two-months-of-llm-traffic-measured) of this series had one governing rule: **you don't know if any of it works until you measure**. 

That rule applies here too, and the surface to measure is the install itself. We'll be honest up front: the catalog is new, so what we have is instrumentation, not results. So, nothing to brag about yet. Still, here's the instrumentation anyway, because the time to build it is _before_ the traffic arrives, not after.

We can't see a `curl` that runs on someone's laptop, but we can see every interaction with the install UI. Each button on a skill card fires a tracked event: an install command copied from the button (tagged with the target agent and the method), a command selected and copied by hand, a `SKILL.md` body copied whole, an archive downloaded, and a click through to the source on GitHub. 

When a skill card is embedded inside a blog post, every event is suffixed with the post's slug, so a copy or a download can be attributed to the article that drove it. That's the segmentation [part two](/chronicles/which-ai-actually-reads-your-site-two-months-of-llm-traffic-measured) showed you need before you trust any number: which skills get adopted, from which posts, into which agents.

The same server-side technique from part two applies to the payload fetches themselves. Requests to `/.well-known/agent-skills/index.json` and to each skill's `url` are ordinary HTTP hits you can classify by `User-Agent` at the edge, which is where to look if you want to know which agents resolve your skills rather than which humans copy your commands. 

And as part two warned, split your user-agent buckets before you believe the totals: our own numbers only made sense once ChatGPT and Claude Code were counted separately, because they turned out to want opposite things.

## FAQ

### What is an agent skill?

A `SKILL.md` file (plus optional extra files) that teaches an AI coding agent a repeatable practice. YAML front matter gives it a `name` and a `description` that says when to use it, and the Markdown body says how. Anthropic introduced the format for Claude, and most major agents (Cursor, Codex, GitHub Copilot, Gemini CLI) now read it too. It's a plain file, with no runtime.

### Do agent skills work outside Claude Code?

Yes. 

`SKILL.md` is a cross-agent format, the payload is identical everywhere, and only the destination directory changes. Inside a project, that directory is converging on a shared `.agents/skills/`, which Cursor, Codex, GitHub Copilot, and Gemini CLI all read, with Claude Code the holdout at `.claude/skills/`. Installed globally, each agent still keeps its own home: `~/.claude/skills`, `~/.cursor/skills`, `~/.codex/skills`, `~/.copilot/skills`, `~/.gemini/skills`. The `npx skills` installer maps 70+ agents onto those paths for you, which is the argument for publishing one discovery index rather than a per-agent bundle five times over.

### How do I make my agent skills discoverable to LLMs?

Publish a `.well-known/agent-skills/index.json` at your domain. 

This is a list of your skills, each with a `name`, `description`, `type` (`skill-md` or `archive`), a `url` to the payload, and a `sha256:` digest of that payload. The [`npx skills` installer](https://github.com/vercel-labs/skills) reads it when someone runs `npx skills add https://yourdomain.com/agent-skills --skill <name>`—the full `https://` URL is what points the installer at your index instead of trying to clone a repo. It's the skills equivalent of `llms.txt`: a well-known, machine-readable list at a path agents already check.

### How do I share my agent skills with other developers?

Publish them so any agent's installer can resolve them, instead of emailing a file around. 

Put each skill's `SKILL.md` in a GitHub repo, then serve a `.well-known/agent-skills/index.json` discovery index at your own domain listing each skill's `name`, `description`, `type`, `url`, and `sha256:` digest. Once that's live, anyone can install a skill with `npx skills add https://yourdomain.com/agent-skills --skill <name>`, `gh skill install`, or a Claude plugin. Re-host the payloads on your domain rather than linking raw GitHub URLs, so the integrity digest matches the exact bytes you serve. That's how our own catalog at [/agent-skills](/agent-skills) is shared.

### Do I have to host skills on GitHub?

For the repo-based install paths, yes: `gh skill install` and Claude's plugin marketplace both resolve straight from a GitHub repo, and the plugin `@name` comes from the repo's `.claude-plugin/marketplace.json`. But don't point your discovery index at raw GitHub URLs. Re-host the payloads on your own domain and compute the digest over the bytes you serve, so an upstream change can't break the integrity check behind a hash you already published.

### What's the difference between a single-file skill and a bundle?

A single-file skill is one `SKILL.md`, served directly and marked `type: skill-md`. A multi-file skill ships extra files and is packed into a `type: archive` tarball. A bundle is one repo publishing several skills at once: each skill gets its own discovery entry and installs individually, and the bundle also offers a combined archive so `curl … | tar` can install the whole set in one command. 

As an example, our `storybook-workbench` bundle is eleven skills in one repo.

### Why offer more than one install command?

Because your readers live in different tools. A Claude Code user wants the plugin marketplace. Someone scripting a container wants a dependency-free `curl` line. Someone who'd rather install from the repo they can read than from our domain wants `gh skill`. A less technical reader wants one copy-paste command for the tool they already have open. 

The `SKILL.md` payload is identical across agents, so the cost of offering four paths is a few command strings, and the payoff is not losing the reader who didn't use your default.

### Is a discovery index the same as llms.txt?

Same *pattern*, pointed at a new payload. 

`llms.txt` (from [part one](/chronicles/how-to-make-your-website-visible-to-llms)) points agents at your content in Markdown. `.well-known/agent-skills/index.json` points them at installable skills, with integrity digests so an installer can verify what it downloaded. Both are machine-readable lists at a well-known path. If you already shipped one, the other is the same reflex applied to a new artifact.

*Download Evil Martians' agent skill*: https://github.com/evilmartians/agent-skills

Agent skills are young, and the conventions around them will shift, the way `llms.txt` and content negotiation are still shifting. But the shape of the answer is already familiar to us, because it's what package registries have always done: a well-known index, a canonical URL per artifact, and a checksum the client verifies before it installs anything. 

[Evil Martians](/) has been shipping into that model for years across npm, RubyGems, and Docker Hub: 128 open source projects, 186K+ GitHub stars, 25B+ downloads. Publishing skills didn't ask a new discipline of us, just an old one pointed at a new kind of consumer. The agents are already looking for the file. The only question is whether yours is there when they check!

---

**AI-ready** Let’s run a 2-week LLM-optimization sprint with Evil Martians to improve your discoverability. [Contact Evil Martians](https://evilmartians.com/contact-us)
