# 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.

Source: https://voxgig.com/howto/organise-skills-across-a-multi-package-ecosystem

- Audience: devrel
- Level: intermediate
- Verified: 2026-09-14
- Published: 2026-09-14
- Updated: 2026-09-24

## 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](https://code.claude.com/docs/en/skills) describes, which is what makes this resolvable with
a directory walk. The [frontmatter fields](https://code.claude.com/docs/en/skills#skill-structure)
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

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [A Claude Code plugin marketplace repo](https://code.claude.com/docs/en/plugin-marketplaces) | You want versioned distribution and installation across many machines | A marketplace to publish and versions to manage, which is more than a copy | The repositories are yours and always checked out together |
| [A shared skills repository](https://code.claude.com/docs/en/skills) | Several repositories with one owner and one set of conventions | A second checkout, or a submodule, in every repository that needs it | Only one repository exists |
| [Layered resolution with a lint](https://nodejs.org/api/fs.html#fsreaddirsyncpath-options) | Any multi-repository setup, as the thing that makes the layers visible | A resolver to run, and findings somebody has to act on | Nothing: this is how you see what the layers produced |
| [A skills folder in each repository](https://code.claude.com/docs/en/skills) | One repository, or conventions that genuinely differ everywhere | Copies that drift, with no way to tell which one an agent used | The 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.

```js title="resolve.mjs"
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.

```js title="resolve.mjs"
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.

```bash
node demo.mjs
```

```text output
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.

```bash
node --test resolve.test.mjs
```

```text output
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.

## Related how-tos

- [Name tools so an agent picks the right one from fifty](/howto/name-tools-so-agents-pick-the-right-one)

- [Set robots.txt rules for each AI crawler](/howto/set-robots-rules-for-ai-crawlers)

## Last verified

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

[^1]: The [specification](https://agentskills.io/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](https://code.claude.com/docs/en/skills) 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.

[^2]: Claude Code made the same call for its own layers. Its
[documentation](https://code.claude.com/docs/en/skills) 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.

[^3]: The argument was settled upstream. The [specification](https://agentskills.io/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.