How-to › Expose your API to agents

How to organize agent skills across many related repositories#

Keep one shared set of skills for the ecosystem, let each repository override what it needs, and report every shadowed name rather than letting one win in silence.

Audience
DevRel
Level
intermediate
Topic
Write agent skills and instruction files
Verified

Your ecosystem has fourteen repositories and every one carries a copy of the same release skill. Three of them are out of date and two disagree about the changelog format. That matters because nobody can say which copy an agent used when it got a release wrong. The skill was right when it was written, once, in a repository nobody remembers.

What you get

You will end up with one shared layer, per-repository overrides, and a report of which skills an agent in a given repository actually sees. You also get a lint that fails a skill an agent cannot choose. This is for you if you maintain several repositories that share conventions.

Short answer

Put the skills that apply everywhere in one shared place, and keep per-repository skills in that repository. Resolve them as layers, with the local one winning, and report every name that was shadowed. Lint both layers: a skill needs a name matching its directory and a description long enough for an agent to choose on.

You will need

Node 22 or later, and more than one repository whose agents need the same conventions. A skill is a directory with a SKILL.md carrying frontmatter, as the Agent Skills format describes, which is what makes this resolvable with a directory walk. The frontmatter fields are the part the lint below reads.1

Voxgig maintains tabnas, whose repositories share skills this way. This page compares that with a plugin marketplace and with leaving every repository on its own.

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
A Claude Code plugin marketplace repoYou want versioned distribution and installation across many machinesA marketplace to publish and versions to manage, which is more than a copyThe repositories are yours and always checked out together
A shared skills repositorySeveral repositories with one owner and one set of conventionsA second checkout, or a submodule, in every repository that needs itOnly one repository exists
Layered resolution with a lintAny multi-repository setup, as the thing that makes the layers visibleA resolver to run, and findings somebody has to act onNothing: this is how you see what the layers produced
A skills folder in each repositoryOne repository, or conventions that genuinely differ everywhereCopies that drift, with no way to tell which one an agent usedThe same skill exists in three repositories

The question is not whether to share. It is what happens when the shared version and the local one disagree, and the answer has to be visible rather than accidental. A local skill winning is the right default, because the repository knows its own source layout, and a resolver that reports the shadowing is what stops a silent override becoming a mystery.2

A marketplace and a shared repository differ on distribution rather than on structure. If your repositories are always checked out side by side, a shared directory costs nothing. If they are installed independently, on machines you do not control, versioned distribution starts earning its keep.

Resolve the layers, and report the shadowing

The resolver merges two layers, lets the last one win, and records every override.

for (const layer of layers) {
  for (const skill of layer.skills) {
    // A skill with no name in its frontmatter is keyed by its directory, so
    // it still appears and still fails the lint rather than vanishing.
    const key = skill.name ?? skill.dir
    if (merged.has(key)) shadowed.push({ name: key, by: layer.source, over: merged.get(key).source })
    merged.set(key, { ...skill, source: layer.source })
  }
}

Keying a nameless skill by its directory matters more than it looks. The alternative is a skill that disappears from the merge because its key was undefined, which reads as a skill that was never written.

Record where each surviving skill came from. The question after a bad agent run is which version it read, and a resolver that answers it turns that into a lookup.

Lint what an agent has to choose from

Each of the three rules is about the moment an agent picks.

if (!skill.name) findings.push(`${skill.dir}: no name in the frontmatter, so nothing can address it`)
else if (skill.name !== skill.dir) findings.push(`${skill.dir}: name is ${skill.name}, which differs from its directory`)
if (!skill.description) findings.push(`${skill.dir}: no description, so an agent cannot tell when to use it`)

A name that differs from its directory is the rule people argue with, and it earns its place the first time somebody greps for a skill by name and finds nothing.3

Run the lint over the shared layer on its own as well as over each merge. A finding in the shared layer affects every repository, and it should not be reported fourteen times.

Check it worked

Resolve for two repositories: one with an override, and one without.

node demo.mjs
repo-parser: 3 skills
  debug-grammar (from repo-parser)
  parse-json (from repo-parser)
  release (from shared)
  shadowed: parse-json from shared replaced by repo-parser
repo-abnf: 3 skills
  compile-abnf (from repo-abnf)
  parse-json (from shared)
  release (from shared)
  finding: compile-abnf: no name in the frontmatter, so nothing can address it

lint of the shared layer alone
  no findings

The parser repository overrides parse-json because inside that repository the engine is the local source rather than the published package, which is exactly the kind of thing a shared skill cannot know. The shadowing is reported, so nobody has to guess which version applied.

The second repository shows the other half. Its own skill has no name field, it still appears in the listing, and the lint says why it cannot be addressed. A resolver that dropped it would leave somebody wondering where their skill went, and the answer would be nowhere visible.

node --test resolve.test.mjs
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 125.614828

When it goes wrong

An agent used the wrong version of a skill. Two layers used the same name without a warning. Run the resolver in the repository and read the shadowed list.

The shared skills are stale. Nobody owns them, because they belong to every repository and therefore to none. Give the shared layer a home repository with reviewers.

A shared skill references a path that only exists in one repository. It was written in that repository and moved without being generalized. Keep the shared layer free of paths, or make the path a parameter the repository supplies.

Every repository overrides everything. The shared layer is describing conventions that are not actually shared. Move those skills back down and keep the shared layer small. Three skills everybody uses beats twenty that everybody overrides.

When not to do this

Do not share a skill that is right in one repository and merely plausible in the others. A skill that is slightly wrong everywhere costs more than four copies that are each correct.

Do not put secrets or host names in a shared skill. It is copied into every checkout, and the blast radius of a shared file is the whole ecosystem.

Do not build a marketplace for repositories that are always checked out together. The distribution machinery is real work, and a shared directory already solves the problem you have.

Do not copy the tabnas layout because it works for tabnas. Those repositories are checked out together, by the same people, and share one release process. A shared layer is right there and wrong for an ecosystem whose repositories are installed independently by people you have never met.

Last verified

Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.

Footnotes

  1. The specification puts numbers on both fields. A name is 1 to 64 characters of lowercase letters, digits and hyphens, with no hyphen at either end and no two together, and a description is 1 to 1,024 characters. Claude Code adds two rules of its own. A field it does not recognize is ignored without a report, and the listing an agent chooses from caps the combined description and when_to_use text at 1,536 characters. A skill can pass the standard in full and reach the agent in part. ↩︎ Back to text

  2. Claude Code made the same call for its own layers. Its documentation gives the example of a skill named Commit synced from claude.ai and a local skill named commit, which count as the same name, so the local one keeps running. The comparison ignores case, spacing and invisible characters, and treats full-width letters and dash variants as their plain forms. A name that differs by a look-alike letter from another alphabet counts as a different name, and a label is how the two are told apart. ↩︎ Back to text

  3. The argument was settled upstream. The specification lists, among the constraints on name, that it must match the parent directory name, in the same breath as the ban on consecutive hyphens. A lint that enforces it is enforcing the standard, which is a more comfortable position than enforcing a preference. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Get the Voxgig dispatch

Short notes on building SDKs, CLIs, REPLs, and MCPs for API-first teams, plus the occasional Fireside episode pick.

By signing up you agree to our Terms and Conditions.