Documentation
A resolver for agent skills
Install SkillAppMD once into your coding agent. From then on it works out which SKILL.md you need for the task in front of you, instead of you going looking.
What SkillAppMD is
An index of 3,797,117 SKILL.md occurrences from 282,200 public repositories, recording for each one where it came from and whether you are allowed to use it.
It is not a package registry and not a file host. The question it answers is not “what does this skill say” but “where did it come from, and may I use it”.
That framing decides everything below. Because SkillAppMD never holds the answer to the first question, it can be honest about the second.
Install
SkillAppMD ships as a single SKILL.md. Any agent that loads skills can load it: Claude Code, Cursor, Codex, Windsurf and others.
npx skillappmd@latest initYou are installing the resolver, not the index. Nothing lands on your machine beyond that one file.
How resolution works
- 01Your agent hits a gapIt reaches a task needing a capability it does not have.
- 02It asks SkillAppMDDescribing the task, not naming a package.
- 03SkillAppMD answers with candidatesA short list, each with its source repository and rights state.
- 04Your agent fetches from originStraight from the source repository. Content never passes through SkillAppMD.
Step four is the one that matters. SkillAppMD never serves third-party skill content, so the fetch happens between your agent and the origin. What SkillAppMD contributes is the pointer and the licence position.
Identity and duplicates
The same SKILL.md is copied between repositories constantly. Roughly half of all occurrences measured were duplicates of something already seen, so an index that treated every copy as a separate skill would overstate the ecosystem by about two times.
Each record therefore carries two hashes, and the difference between them is the whole deduplication story.
"identity": {
"content_hash": "sha256:c2e4a73a…", // the raw bytes, exactly as found
"normalised_hash": "sha256:bd17276e…" // after line endings, trailing
} // whitespace and key order settleTwo files with the same content_hash are byte-identical. Two files with the same normalised_hash but different content_hash are the same skill saved by a different editor. Both collapse to one canonical record; every copy survives as an occurrence, which is what /api/v1/skills/:id/occurrences returns.
Nothing is discarded in the collapse. If you need to know that a skill appears in forty repositories, that is a query rather than a guess.
How licence is decided
Licence is not one field, because in reality it is not one fact. Three layers are recorded separately and never merged into a single verdict you cannot audit.
- Dataset
l1_datasetThe licence of the corpus the record was discovered through.- Repository
l2_repositoryThe LICENSE file of the repository the file lives in. This is the layer that usually decides.- Declared
l3_declaredA licence claimed in the SKILL.md frontmatter. A claim, not proof: a file cannot license itself out of its repository.
Where the layers disagree, conflict is set and the disagreement stays visible. A record whose frontmatter claims MIT inside a repository with no LICENSE file is not quietly promoted to MIT — the claim is recorded as a claim.
This matters more than it sounds: 47.8% of repositories carry no licence at all, measured across 3,074 real repositories. A model that forced a single answer would be inventing one for nearly half the index.
Rights states
There are exactly two states. Whether you may redistribute is a separate field, because it is a separate question.
- Licensed
known - A licence was resolved. `basis` names which layer decided it, and `redistributable` says what that licence permits.
- Unknown
unknown - No licence could be determined. Not a refusal, and not the same as forbidden.
You will notice there is no restricted state. A licence that forbids redistribution is still a known licence — it reports state: "known" with redistributable: false. Folding those into one label would destroy the distinction between “we know you may not” and “we do not know”.
That distinction is the point. unknown never means forbidden; it means SkillAppMD could not determine the licence and you should check the source before reusing the file. Measured over 4,665 records from 3,074 repositories, 60.1% resolve to unknown — so this is the ordinary case, not a rare edge, and collapsing it into a false boolean would misrepresent most of the index.
API
Six read-only endpoints. There is no write surface: ingestion runs as a batch job, not over HTTP.
/api/v1/healthLiveness and the live schema version./api/v1/skillsList indexed skills. Cursor paginated./api/v1/skills/:idOne skill: declared fields, licence, rights, attribution./api/v1/skills/:id/occurrencesWhere else the same file was found./api/v1/sources/:idOne source and its declared access policy./api/v1/search?q=Keyword search over canonical metadata. Cursor paginated.Response envelope
Every successful response has the same four keys at the top level. Attribution lives inside the record, not beside it, because it belongs to the record: a skill cannot be served without it.
{
"data": { … }, // the record, or an array of records
"meta": { "request_id": "…", "generated_at": "…" },
"cursor": { "next": "opaque-or-null", "limit": 50 }, // collections only
"notice": "…"
}And inside a skill record, the parts a consumer usually wants:
{
"declared": { "name", "description", "frontmatter", "allowed_tools" },
"inferred": { },
"identity": { "content_hash", "normalised_hash" },
"licence": { "l1_dataset", "l2_repository", "l3_declared", "conflict" },
"rights": { "state", "indexable", "linkable",
"redistributable", "cacheable", "basis", "computed_at" },
"temporal": { "first_commit_at", "last_commit_at",
"discovered_at", "last_verified_at" },
"provenance": { "sources", "field_origins" },
"attribution": { "repository", "owner", "canonical_source_url" },
"content": null,
"content_available": false
}Facts and inferences stay separate
declared holds what the source file states. inferred holds what SkillAppMD worked out. They are separate objects so a consumer cannot read one as the other by accident. Today inferred is empty, by design.
The finer-grained version is provenance.field_origins, which labels each field with its origin — either source_fact: and where it came from, or appmd_inference: and the analyser and version that produced it.
"field_origins": {
"declared.name": "source_fact:gitskills#frontmatter.name",
"licence.l2_repository": "source_fact:gitskills#repos.license",
"identity.content_hash": "appmd_inference:fingerprint@0.1.0",
"rights.redistributable": "appmd_inference:rights-engine@0.1.0"
}Every SkillAppMD judgement therefore arrives with the version that made it. When an analyser changes, the records it touched are a query rather than a guess.
Pagination
Cursors are opaque and there is no offset parameter, because offset pagination is incorrect under concurrent writes. There is no total either, so a client cannot show a page count. When cursor.next is null, you have reached the end. Page size is capped at 100 and defaults to 50.
Errors and rate limiting
Errors return a stable machine readable code, a human message and a request id. Codes are the contract; messages are not. Exceeding the request budget returns 429 with a Retry-After header.
Caching
Responses carry ordinary HTTP cache directives and a strong ETag. Send it back as If-None-Match and an unchanged representation returns 304 with no body.
Cache-Control: public, max-age=300, must-revalidate
ETag: "e0cf892928f391420d37091455012c66"What is unusual is what decides it. A response is only publicly cacheable if every record in it has a known licence. One unknown-rights record makes the whole page no-store — a page is a single representation and cannot be partially evicted. Given that most records resolve to unknown, expect no-store to be the common answer.
The reasoning is the same as everywhere else on this page: SkillAppMD should not push work whose licence it could not determine into caches it does not control.
Lifetimes are deliberately short — 300 seconds for a single record, 60 for a collection — because an author removal takes effect immediately at the origin but a cached copy survives until it expires. The cache lifetime is the removal latency, so it is bounded rather than tuned for speed.
One consequence worth knowing: a cached response replays the request_id of the request that produced it, so several clients can see the same id. For a cached response meta.generated_at is the more useful field — it tells you how old the representation is.
Boundaries
Four things SkillAppMD will not do, by design rather than by omission.
Serve skill content
The body of a SKILL.md is never returned, whatever its licence.
Rank by popularity
SkillAppMD does not distribute skills, so it cannot count installs.
Publish a trust score
A score without its evidence is not representable. Absence of findings is never reported as safe.
Certify anything
Indexing is not endorsement. Each skill remains under its own repository licence.
There is also a removal path. If you are the author of an indexed file and you want it gone, the request is recorded with its actor, disposition and timestamp, and the bytes are deleted. The provenance envelope and the attribution survive, so the record of the removal cannot itself be quietly removed.
Status
The index and the read API are live. The resolver is published on npm, so the install command above works. Semantic resolution, capability matching and composition are planned and not yet built.
The index currently holds a small number of seed records rather than the full corpus. The ingestion pipeline has been verified against 10,000 real records and re-runs byte-identically, but has not yet been run at scale against the live database. The corpus figures on this page come from our own measurements, not from any other index.