skills.sh: repository layout, selective install and telemetry
This is an investigation
A longer investigation, recorded as findings rather than conclusions. A survey is a claim about what existed when it was written.
Context - setting up a repository to hold several reusable agent skills, one per directory,
distributed through the skills CLI. Three questions had to
be answered before the layout was fixed, because changing it later is expensive: where does
the installer look for skills, how does a user install one rather than all of them, and what
does the CLI report back about the install?
- Version read:
1.7.0, commit7407f38, 2026-09-171 - Method: source read of
src/skills.ts,src/source-parser.ts,src/frontmatter.ts,src/telemetry.tsand the telemetry call sites insrc/add.ts, checked against the README; then empirical confirmation by building a two-skill repository in the layout under question and installing from it, selectively and wholesale, into scratch projects.
Layout: the repository root is a first-class location¶
discoverSkills() walks a priority list of locations rather than searching indiscriminately.
The list begins with the repository root, then skills/ and its .curated,
.experimental and .system variants, then around thirty agent directories such as
.claude/skills.
The depth differs between them, and this is the part that decides a layout. Known containers
like skills/ are walked three levels deep, so skills/<category>/<skill>/SKILL.md is found.
The root is walked one level deep, deliberately, with a comment in the source saying why:
descending further from the root would surface unrelated SKILL.md files, an
examples/foo/SKILL.md being the case named. A --full-depth flag and a depth-five recursive
fallback exist for anything that neither pass found.
So <skill-name>/SKILL.md at the repository root is not a layout the tool tolerates, it is
exactly the depth-one case the root scan is written for. Confirmed empirically: two such
directories produced Found 2 skills.
There is no repository manifest, and nothing to add. A Claude Code plugin.json or
marketplace.json is read if one happens to be present, purely as an additional hint about
where to look. Absent, discovery is entirely filesystem-driven.
Selective install is a first-class path, not a workaround¶
npx skills add owner/repo --skill one-skill # one, by name
npx skills add owner/repo@one-skill # the same, in shorthand
npx skills add owner/repo --skill '*' # all of them
npx skills add owner/repo --list # preview without installing
npx skills add owner/repo#v1.2.0 # pinned to a tag, branch or full SHA
--skill is repeatable, --agent narrows the targets, -y makes it non-interactive and
--all is shorthand for all skills into all agents. Both selective and wholesale installs
were run against the scratch repository; a skill's references/ subdirectory travelled with
it intact.
The frontmatter contract, and the one gap in it¶
parseSkillMd requires name and description, and requires both to be strings rather
than whatever YAML made of them: name: 2026 parses as a number and is refused. A file
failing either check is skipped with a warning on stderr, not treated as an error. Nothing
else in the frontmatter is required. metadata.internal: true hides a skill unless it is
asked for by name.
That a missing field produces a warning rather than a failure is the argument for checking it at commit time. A skill that is quietly invisible to the installer looks correct in every other tool that reads it, which is the shape of defect a pre-commit hook exists to catch.
The gap is what the two names are allowed to do to each other. The installed directory is
named after the frontmatter name, not the source directory. A directory some-directory/
whose SKILL.md declares name: totally-different-name installs to
.claude/skills/totally-different-name/, which was confirmed rather than inferred. Nothing
warns about the disagreement. Since the directory name is also the handle a user types after
@, letting the two drift produces a skill that is installed under one name and requested by
another, and a repository holding several skills is precisely where that stops being
noticeable.
Telemetry: what is sent, and how to stop it¶
The CLI reports installs to https://add-skill.vercel.sh/t, which is what ranks the skills.sh
leaderboard. An install event carries the source as owner/repo, the names of the skills
installed, the agents they were installed into, a global flag, the CLI version, the detected
agent and a ci=1 flag when it recognises a CI environment. Remove, update, find and sync
events are reported too, with their own fields.
It is disabled by either of two environment variables:
DISABLE_TELEMETRY=1
DO_NOT_TRACK=1
isEnabled() is a single negation of both, checked by every send. Honouring DO_NOT_TRACK
means the machine-wide setting already works without the tool having to be configured
specially, which is the better of the two to set.
Three qualifications worth knowing before switching it off or leaving it on.
Private GitHub repositories are already excluded, and the check fails closed. Before
sending, the CLI asks the GitHub API whether the repository is public and sends only when
the answer is an explicit false; an error or an indeterminate answer skips the event. GitHub
Enterprise sources are excluded by the same mechanism, with a comment saying the public API
must not be told an Enterprise repository's name. A private repository therefore does not leak
its name through this path by default.
The exclusion is keyed on parsing owner/repo, and what fails to parse is sent anyway.
The fallback branch reports the source when it cannot be split into exactly one owner and one
repository, on the reasoning that non-GitHub sources cannot be privacy-checked. A GitLab
project inside a subgroup has two slashes and does not match, so a private
group/subgroup/repo reaches the telemetry endpoint as a string. The default is safe for
GitHub and is not safe in general.
Opting out also disables the security audit lookup. fetchAuditData guards on the same
isEnabled(), so a machine with DO_NOT_TRACK set no longer fetches the partner risk ratings
that would otherwise be shown before an install. That is a real trade rather than a bug: the
audit call necessarily names the skills being installed, so it could not be exempted without
defeating the opt-out. It is worth knowing that the quieter machine is also the one told less
about what it is about to run.
Bottom line¶
A repository of one skill per root-level directory needs no manifest, no skills/ wrapper and
no publishing step, and users can take one skill or all of them from it, at a pinned revision,
with the authentication they already have. Two things are worth adding around it rather than
trusting to review: a commit-time check that SKILL.md carries a string name and
description, since the installer only warns, and a check that the name matches its
directory, since nothing anywhere else will notice when it does not.
Telemetry is on by default, reports the repository and the skill names, and is switched off by
DO_NOT_TRACK=1. For anything not public, set it regardless of the built-in private-repository
exclusion: that exclusion is a GitHub-shaped check with a documented hole in it, and an
environment variable is not.
Backlinks¶
The following pages link to this page:
-
vercel-labs/skills: install agent skills from any git repository, last modified 2026-09-18 ↩