# Vat Skill Distribution

> Use when setting up `vat build`, configuring plugin distribution (marketplace, plugins, managed settings), npm publishing with postinstall hooks, or `vat verify` — the full pipeline from skill source to installed plugin.

- Skill: `jdutton/vat-skill-distribution` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jdutton/vat-skill-distribution`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jdutton/vat-skill-distribution/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: jdutton (https://skillmd.com/u/jdutton)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/jdutton/vat-skill-distribution

---


# VAT Distribution: Build, Publish & Install

## Scope

This skill covers the **file-based install method for Claude Code CLI** (`~/.claude/plugins/`).
This is the only install method VAT currently supports.

For the full install landscape — Claude Desktop paths, enterprise CI deployment,
Anthropic Cloud org management, MDM integration, and the `vat claude plugin uninstall`
design — see the contributor reference at `docs/contributing/vat-install-architecture.md`
in the `vibe-agent-toolkit` repo (contributor material, not bundled with this skill).

## Overview

VAT distributes skills as **Claude plugins** via npm packages. The pipeline:

1. `vat build` compiles SKILL.md sources into plugin artifacts
2. `npm publish` pushes the package to a registry
3. `npm install` triggers a postinstall hook that registers the plugin in Claude Code's plugin system

Skills installed this way appear in Claude Code as `/plugin-name:skill-name`.

## Project Structure

```
my-project/
├── package.json                    ← vat.skills + postinstall hook + publishConfig
├── vibe-agent-toolkit.config.yaml  ← skills: + claude: config
├── resources/
│   └── skills/
│       └── SKILL.md
└── dist/                           ← generated by vat build
    ├── skills/my-skill/            ← packaged skill
    └── .claude/plugins/marketplaces/
        └── my-marketplace/plugins/my-plugin/
            ├── .claude-plugin/plugin.json
            └── skills/my-skill/SKILL.md
```

## Step 1: package.json Configuration

```json
{
  "name": "@myorg/my-skills",
  "version": "1.0.0",
  "vat": {
    "version": "1.0",
    "skills": ["my-skill"]
  },
  "dependencies": {
    "vibe-agent-toolkit": "latest"
  },
  "scripts": {
    "build:vat": "vat build",
    "postinstall": "vat claude plugin install --npm-postinstall 2>/dev/null || exit 0"
  },
  "files": ["dist", "README.md"],
  "publishConfig": {
    "registry": "https://registry.npmjs.org"
  }
}
```

**`vibe-agent-toolkit` must be in `dependencies` (not `devDependencies`).** npm adds all `bin` entries from runtime dependencies to `./node_modules/.bin/` and puts that on PATH when running lifecycle scripts. This is how `vat` is available during your postinstall without being globally installed. If it is in `devDependencies`, npm will not install it on the user's machine and the postinstall will fail with "command not found".

The `vat.skills` array contains skill name strings for npm discoverability. Skill source paths and packaging config live in `vibe-agent-toolkit.config.yaml` (see Step 2).

For private GitHub Packages registry:
```json
"publishConfig": {
  "registry": "https://npm.pkg.github.com",
  "access": "restricted"
}
```

Users (or IT) installing from GitHub Packages need `.npmrc` configured with the scope registry and a read-only token:
```
@myorg:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
```

IT deploying to managed machines should pre-configure `.npmrc` at the system or user level before running install commands. End users do not need to understand npm or the registry — IT handles it once.

## Handling Plugin Renames: vat.replaces

> **Use this only when renaming a plugin, merging plugins, or cleaning up legacy flat-skill installs. Normal upgrades (same plugin name, same skills) do NOT need `vat.replaces` — the installer already overwrites the plugin directory on every install.**

### The problem: silent stale skills

When you rename a plugin or reorganize skills across packages, the old registration remains in Claude Code. Claude Code **does not warn you** when two plugins provide conflicting skill names — the first-registered (stale) skill silently wins. Users continue loading old content from the renamed plugin unless the old registration is explicitly removed.

This is the scenario where `vat.replaces` is needed:
- Plugin renamed: `old-plugin-name` → `new-plugin-name`
- Two old plugins merged into one new plugin
- Skills previously installed to `~/.claude/skills/<name>` (legacy pre-0.1.20 flat install) now delivered via the plugin tree

### How it works

When a VAT package is installed (via postinstall hook or `--dev`), the installer reads `vat.replaces` from `package.json` and — **before** installing the new plugin:

1. For each name in `replaces.plugins`: uninstalls `<name>@<marketplace>` — removes plugin directory, cache entry, registry entry, and `settings.json` entry
2. For each name in `replaces.flatSkills`: deletes `~/.claude/skills/<name>` — removes legacy pre-0.1.20 flat installs

Both operations are idempotent — "not found" is handled gracefully.

### Schema

```json
"vat": {
  "version": "1.0",
  "skills": ["authoring", "audit"],
  "replaces": {
    "plugins": ["my-old-plugin-name"],
    "flatSkills": ["my-old-skill", "another-old-skill"]
  }
}
```

Both `plugins` and `flatSkills` are optional arrays. The entire `replaces` key is optional — omit it when there is nothing to clean up.

### Real example: vat-development-agents 0.1.21

This package (`vat-development-agents`) renamed its plugin from `vat-development-agents` to `vibe-agent-toolkit` in v0.1.21. Without `vat.replaces`, users who had already installed v0.1.20 would have both `vat-development-agents@vat-skills` and `vibe-agent-toolkit@vat-skills` registered — Claude Code would serve stale skill content from the old plugin.

The fix in `package.json`:

```json
"vat": {
  "version": "1.0",
  "skills": ["vibe-agent-toolkit", "resources", "distribution", "authoring", "audit", "debugging", "install"],
  "replaces": {
    "plugins": ["vat-development-agents"],
    "flatSkills": ["vibe-agent-toolkit", "resources"]
  }
}
```

- `plugins`: removes the old plugin registration (same marketplace, old plugin name)
- `flatSkills`: removes legacy `~/.claude/skills/vibe-agent-toolkit` and `~/.claude/skills/resources` entries from users who installed before 0.1.20 switched to the plugin tree

### Non-obvious gotchas

**1. Plugin names in `replaces.plugins` have NO `@marketplace`**

The format is just the plugin name — e.g. `"vat-development-agents"` — NOT `"vat-development-agents@vat-skills"`. The installer infers the marketplace from the current package's dist tree. Using `@marketplace` syntax here would be wrong.

**2. `replaces.plugins` is for old plugin registrations, not for skills that moved between plugins**

If a skill moved from `plugin-a` to `plugin-b` within the same marketplace, list `"plugin-a"` in `replaces.plugins` to clean up the entire old plugin. You do not manage individual skills — you manage plugins.

**3. `replaces.flatSkills` is ONLY for the legacy `~/.claude/skills/` location**

This is specifically for skills that were previously installed as flat files to `~/.claude/skills/<name>` (pre-plugin-tree, before v0.1.20). Skills within the plugin tree (under `~/.claude/plugins/`) are handled via `replaces.plugins`. Do not mix them up.

**4. Normal upgrades need nothing**

If you publish a new version of the same package with the same plugin name, the installer overwrites the plugin directory automatically. `vat.replaces` is only for the case where the old name is different from the new name.

**5. The symptom is subtle and delayed**

You will not see an error. Claude Code simply loads the first-registered skill with a given name. If the stale plugin is registered first (alphabetically or by install order), your new content is invisible until you remove the old registration.

## Step 2: vibe-agent-toolkit.config.yaml

```yaml
version: 1

skills:
  include:
    - "resources/skills/**/SKILL.md"

claude:
  marketplaces:
    my-marketplace:                   # org/publisher identity
      owner:
        name: My Organization
      plugins:
        - name: my-plugin             # installable unit
          description: My plugin description
```

The top-level `skills:` section drives standalone skill builds (output: `dist/skills/`). The `claude:` section defines plugins, which are assembled from their own `plugins/<name>/` directories (plugin-local skills under `plugins/<name>/skills/**/SKILL.md`). Each marketplace has `owner` and `plugins` fields (strict schema — no extra fields).

**Naming convention:** marketplace = org identity (e.g. `acme`), plugin = this package
(e.g. `acme-tools`). Registers as `my-plugin@my-marketplace` in Claude's plugin registry.

### Multiple skills in one plugin

List all skills in `vat.skills` for npm discoverability:

```json
"vat": {
  "version": "1.0",
  "skills": ["my-linting", "my-testing"]
}
```

Each skill lives as a subdirectory of the plugin under `plugins/<name>/skills/<skill>/SKILL.md`:

```
plugins/my-plugin/
  skills/
    my-linting/SKILL.md
    my-testing/SKILL.md
```

All plugin-local skills found under `plugins/<name>/skills/` are packaged into the plugin automatically — no per-plugin selector is needed or supported. Skill names must be globally unique across all plugins.

## Step 3: Build

```bash
vat build        # skills phase then claude phase
vat verify       # validates resources + skills + claude artifacts
```

### What vat build does

Two phases, run in dependency order:

1. **`vat skills build`** — reads `vibe-agent-toolkit.config.yaml skills:` section, discovers SKILL.md files via include/exclude globs, compiles each into `dist/skills/<name>/`
2. **`vat claude plugin build`** — reads `vibe-agent-toolkit.config.yaml claude:` section, wraps built skills into `dist/.claude/plugins/marketplaces/<mp>/plugins/<plugin>/` structure with `.claude-plugin/plugin.json`. Cleans stale output before each build.

Individual commands still work:
```bash
vat skills build            # skills phase only
vat claude plugin build     # claude plugin phase only (requires skills already built)
```

## Step 4: Publish

```bash
npm publish --tag next    # RC/pre-release
npm publish               # stable release
```

## Marketplace Distribution

Marketplace distribution publishes a dedicated branch to GitHub that Claude Code users can install directly — no npm account or registry required.

### How it works

1. `vat build` compiles skills and plugin artifacts into `dist/`
2. `vat claude marketplace publish` pushes the `dist/.claude/` tree to a dedicated branch (e.g. `claude-marketplace`) in your GitHub repo
3. Users install via the slash command: `/plugin marketplace add owner/repo#claude-marketplace`

### Configuration

Add a `publish` section under your marketplace in `vibe-agent-toolkit.config.yaml`:

```yaml
claude:
  marketplaces:
    my-marketplace:
      owner:
        name: My Organization
      plugins:
        - name: my-plugin
          description: My plugin description
      publish:
        github:
          repo: owner/repo          # GitHub repo to publish to
          branch: claude-marketplace # branch that stores the installable artifacts
```

### Publish workflow

```bash
vat build                                    # build all artifacts first
vat claude marketplace publish               # push dist/.claude/ to the publish branch
vat claude marketplace publish --dry-run     # preview what would be published (no push)
```

### Consumer install

Once published, users install with:

```
/plugin marketplace add owner/repo#claude-marketplace
```

No npm, no registry, no token required. Claude Code fetches the branch directly from GitHub.

### Testing locally

After publishing, verify the marketplace works end-to-end:

```bash
claude plugin marketplace add owner/repo#claude-marketplace
claude plugin install my-plugin@my-marketplace
claude plugin validate ~/.claude/plugins/cache/my-marketplace/my-plugin/<version>
claude plugin list   # verify status: enabled
```

Start a new Claude Code session to confirm skills load. See the [Marketplace Distribution Guide](https://github.com/jdutton/vibe-agent-toolkit/blob/main/docs/guides/marketplace-distribution.md#testing-your-marketplace) for the full testing checklist and known issues.

**Note (Claude Code v2.1.81):** If re-adding a marketplace with the same name as an existing one (e.g., switching from npm to GitHub source), remove the old marketplace first: `claude plugin marketplace remove <name>` then re-add. Otherwise the old source is silently reused.

### Per-plugin versioning (multi-plugin marketplaces)

VAT supports two versioning models for a marketplace:

**Single-version (default for skills-only marketplaces).** No `version` is declared on individual plugins. All plugins inherit the root `package.json:version`. This is the model used by `vibe-agent-toolkit` — the marketplace is treated as one release artifact.

**Per-plugin versioning** (multi-plugin marketplaces with independent release cadences). Each plugin declares its own `version`. Recommended when topical plugins under one marketplace evolve on independent timelines.

#### Where to declare a per-plugin version

Two options, in precedence order:

1. **Marketplace config** (`vibe-agent-toolkit.config.yaml`) — most explicit:
   ```yaml
   claude:
     marketplaces:
       my-marketplace:
         plugins:
           - name: ai-digest
             version: 0.2.0
             skills: '*'
   ```
2. **Plugin source** (`plugins/<name>/.claude-plugin/plugin.json:version`) — most ergonomic for plugin authors:
   ```json
   { "name": "ai-digest", "version": "0.2.0", "description": "..." }
   ```

If both declare a version, marketplace config wins and VAT logs a reconciliation warning. If neither is declared, VAT falls back to the root `package.json:version` (single-version model).

#### What `vat claude marketplace publish` does for multi-plugin marketplaces

For each plugin with a resolved version:

- The published `.claude-plugin/marketplace.json` includes the per-plugin `version` field on each plugin entry.
- If `<plugin.source>/CHANGELOG.md` exists in source (or the marketplace plugin entry's `changelog` field points to a file), it is bundled into the published marketplace at `plugins/<name>/CHANGELOG.md`, alongside the marketplace-level CHANGELOG.

The marketplace-level CHANGELOG (under `publish.changelog` in the config) continues to work unchanged.

#### Default CHANGELOG location

The default per-plugin CHANGELOG path is `<plugin.source>/CHANGELOG.md`, anchored to the entry's `source` field (default `plugins/<name>`). It is NOT assumed to be `plugins/<name>/CHANGELOG.md` — if you override `source`, the default CHANGELOG path follows.

To use a non-default path, set the `changelog` field on the marketplace plugin entry (relative to the plugin source dir):

```yaml
plugins:
  - name: ai-digest
    source: plugins/ai-digest
    changelog: docs/RELEASES.md
    skills: '*'
```

#### Backwards compatibility

Marketplaces with no per-plugin version anywhere are unaffected. The root `package.json` version flows through to every plugin, and the published `marketplace.json` either omits per-plugin `version` or includes the same value for all plugins.

### Referencing another marketplace's plugin (`externalSource`)

A plugin entry can point at a plugin published elsewhere (`github`/`url`/`npm`/`pip`,
matching Anthropic's official [Plugin marketplaces](https://code.claude.com/docs/en/plugin-marketplaces)
source shapes) instead of being built here — set `externalSource` with `skills: []` and
omit `source`/`files`/`exclude`/`changelog`; VAT never fetches or vendors it, only writes
the reference into `marketplace.json`. See "Referencing Another Marketplace's Plugin" in
docs/guides/marketplace-distribution.md
for the field reference and a worked example.

## Step 5: User Install

### Recommended: npm global install (postinstall runs automatically)

```bash
npm install -g @myorg/my-skills
```

The postinstall hook fires automatically and registers the plugin in Claude. This is the correct path for IT-managed deployments — no other tools required on the user's machine.

### Developer/IT one-off install via npx

```bash
npx vibe-agent-toolkit claude plugin install npm:@myorg/my-skills
```

Downloads and runs VAT via npx to install a package without a global install. Useful for CI, scripting, or testing from a developer machine. Requires the npm scope registry to be configured (`.npmrc`) if installing from a private registry.

### How plugin installation works

When `npm install` runs the postinstall hook (`vat claude plugin install --npm-postinstall`):

- VAT detects `dist/.claude/plugins/marketplaces/` directory in the installed package
- Copies the plugin tree to Claude's plugin directory (dumb recursive copy)
- Writes to these locations:
  1. `~/.claude/plugins/marketplaces/<marketplace>/plugins/<plugin>/` — plugin files
  2. `~/.claude/plugins/known_marketplaces.json` — marketplace registry
  3. `~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/` — version cache
  4. `~/.claude/plugins/installed_plugins.json` — installation record
  5. `~/.claude/settings.json` `enabledPlugins` — activates the plugin

If no `dist/.claude/plugins/marketplaces/` directory exists (package wasn't built before publish): a guidance message is emitted and the hook exits 0. The publisher must run `vat build` and re-publish.

Skills are then available in Claude Code as `/plugin-name:skill-name`.

## managed-settings.json Validation (Enterprise)

```yaml
claude:
  managedSettings: managed-settings.json
```

`vat verify` validates this file against the ManagedSettings schema. Catches typos and schema errors before deployment. Does NOT deploy the file — deployment is a separate concern.

## --target claude-web (ZIP Upload)

For uploading skills directly to `claude.ai/settings/capabilities`:

```bash
vat skills package ./SKILL.md -o ./dist/ --target claude-web
```

Produces a ZIP:
```
my-skill.zip
└── my-skill/
    ├── SKILL.md             # skill definition (required)
    └── references/          # every auto-discovered bundled file — optional
```

⚠️ **`claude-web` does not route by extension.** The `claude-code` target sorts auto-discovered
files into `resources/`, `scripts/`, `templates/` and `assets/` by extension; `claude-web`
flattens *all* of them into `references/` (`getResourceSubdirForFile` in
`packages/agent-skills/src/skill-packager.ts`). Do not write a skill that expects a
`scripts/foo.py` path in a claude-web ZIP.

To place a file somewhere the automatic routing would not put it — a build artifact, an unlinked
file, or a routing override — declare an explicit source→dest mapping with `files:`. Destinations
are relative to the skill output directory, so they are what fixes the layout for either target:

```yaml
skills:
  include:
    - "skills/**/SKILL.md"
  config:
    my-skill:
      files:
        - source: "dist/helpers/format.mjs"
          dest: "scripts/format.mjs"
        - source: "assets/template.json"
          dest: "assets/template.json"
```

`source` is relative to the project root and may be a glob; `dest` is relative to the skill output
directory and must stay inside it. See `vibe-agent-toolkit:vat-skill-authoring` for the full
`files:` semantics.

⚠️ VAT does **not** compile or bundle TypeScript for you. `source` must point at a file that
already exists at build time — run your own build step first and point `files:` at its output.

## Quick Reference

| Task | Command |
|---|---|
| Validate sources (no build) | `vat validate` |
| Build everything | `vat build` |
| Verify built artifacts (after build) | `vat verify` |
| Build skills only | `vat skills build` |
| Build claude plugin artifacts only | `vat claude plugin build` |
| Install via npm (end user) | `npm install -g @org/pkg` |
| Install via npx (developer/IT) | `npx vibe-agent-toolkit claude plugin install npm:@org/pkg` |
| List installed plugins | `vat claude plugin list` |
| Uninstall a plugin | `vat claude plugin uninstall --all` |
| Package for claude.ai upload | `vat skills package ./SKILL.md -o ./dist/ --target claude-web` |

## Running VAT Without Global Install

```bash
npx vibe-agent-toolkit <command>    # npm/Node.js
bunx vibe-agent-toolkit <command>   # Bun
```

All `vat` commands in this skill work with these alternatives.

## Future: Zero-Dependency Postinstall (Option B)

A planned improvement: `vat build` would bundle the plugin install logic into `dist/postinstall.js` — a fully self-contained script with no npm dependencies. The postinstall script would become simply `node ./dist/postinstall.js`. This eliminates `vibe-agent-toolkit` as a runtime dependency entirely, reducing install footprint for end users. Until then, Option C (runtime `vibe-agent-toolkit` dep) is the correct approach.

## Full-plugin authoring (commands, hooks, agents, MCP)

VAT supports bundling any Claude Code plugin asset — not just skills. Drop the plugin
under `plugins/<name>/` in the same native layout Claude expects. VAT tree-copies
everything (minus `skills/` and `.claude-plugin/`), merges author `plugin.json` with
the identity fields the marketplace config owns, and applies any `files[]` mappings for
artifacts built outside the plugin dir. The config owns `name`, `version`, and the
`author` subfields `owner` can express (`name`, `email`); every other `author` subfield —
`author.url` in particular, which VAT's config has no field for — passes through from
`plugin.json` untouched, and the generated `marketplace.json` republishes that same
merged author. A `files:` entry's `source` may be a glob (`*`, `**`, `?`, `[`) — the entry then fans out to a directory `dest` using prefix-strip + tail-preserve mapping, with the glob resolved only at build time so SKILL.md links to files landing under a glob dest are treated as deferred artifacts at validate time. An optional `integrity: true` flag byte-verifies the copy at build time and (for glob entries) asserts the dest subtree is an exact match. **A glob honors the never-package list; an explicit entry does not** — a glob drops agent-instruction files (`CLAUDE.md`, `AGENTS.md`, …) and, in a skill bundle, navigation files (`README.md`, `index.md`, …), while `source: extras/README.md` still ships because naming a path is an instruction. For the full model — prefix-strip mechanics, late-binding semantics, sibling sources, never-package tiers, and scope limits — see docs/guides/skill-files-and-routing.md.

The plugin tree-copy drops agent-instruction files at any depth but **keeps** `README.md` — a plugin-root README is the plugin's front page. Project-specific junk goes in `exclude: ["scratch/**"]` on the plugin entry.

See docs/guides/marketplace-distribution.md section "Full-plugin authoring".

