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

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, and discoverable through a .well-known index any agent can find. Here’s how to publish yours.
Other parts:
- Making your site visible to LLMs: 6 techniques that work, 8 that don't
- Which AI actually reads your site? Two months of LLM traffic, measured
- Ship agent skills like packages: discovery index, digests, and install sources
An agent skill 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.

Irina Nazarova CEO at Evil Martians
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 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. 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.
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 for Claude in October 2025 and released it as an open standard that December. Cursor, Codex, GitHub Copilot, and Gemini CLI 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. 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, published as one bundle.inertia-rails-skills— another eleven, covering Inertia.js on Rails across React, Vue 3, and Svelte, published from the Inertia Rails project’s own repo.llms-visibility— makes a site readable to LLMs and the coding agents that fetch URLs: Markdown routes,Acceptcontent negotiation, andllms.txt, plus the AI SEO anti-patterns to refuse. It’s part one 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 and imgproxy 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 installer (an open source tool from Vercel Labs, distributed on npm as skills and backed by the 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.
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. It builds on RFC 8615, 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 schema.
The document is a list of entries:
{
"$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 installreadsSKILL.mdstraight 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.jsonat its root is an installable marketplace, and thenamein that manifest is the@marketplacehalf ofclaude 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.gzevery 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 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— 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 theurlmatching the skill’stype, and verifies the digest before anything lands on disk.claude plugin— Claude’s marketplace path:claude plugin marketplace add evilmartians/agent-skills, thenclaude plugin install good-readme@evilmartians. That@half is thenamefrom 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— 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 itsSKILL.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’scurlandtar.
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.jsonshould 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.gzshould listSKILL.mdat 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 yourdescriptionis doing its job.
Measuring what gets installed
Part two 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 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 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 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) 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.
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!

