Skip to content

bundle assemble the export into an installable agent plugin

kac bundle [--no-color]
Option What it does
--no-color Turn colour off. NO_COLOR in the environment does the same.

What it does

bundle assembles what export wrote, plus the .plugin/ tree your corpus keeps beside its records, into a Claude Code plugin directory under .dist/plugin/. A plugin is the corpus packaged so an agent can install it. bundle also writes the marketplace that offers it into .dist/ above, so you can install the result.

What ends up in the plugin depends on what the export carried. A corpus that ships no glossary ships no glossary skill either. The plugin bundle says what decides that, and what stops a run.

Run export first. bundle reads that output, plus the plugin tree and one key of .corpus.yaml. It never loads the corpus itself.

Examples

A built plugin

kac export
kac bundle

bundle prints each file as it writes it. The run closes with what shipped, what was trimmed and why, and how to install it:

wrote .dist/plugin/hooks/breadcrumb
wrote .dist/plugin/hooks/hooks.json
wrote .dist/plugin/skills/corpus-retrieval/SKILL.md
wrote .dist/plugin/skills/glossary-lookup/SKILL.md
wrote .dist/plugin/skills/raise-finding/SKILL.md
wrote .dist/plugin/skills/request-deviation/SKILL.md
bundle: trimmed skills/controls-lookup: the export carries no controls.
bundle: trimmed skills/fix-lookup: the export carries no fixes.
bundle: trimmed skills/policy-lookup: the export carries no policies.
bundle: trimmed skills/process-lookup: the export carries no processes.
bundle: trimmed skills/standards-lookup: the export carries no standards.
bundle: wrote 26 file(s) to .dist/plugin/ as example-libraries 0.5.0. 5 component(s) included, 5 trimmed.
bundle: .dist/ is a marketplace holding it. Install it from a path with:  claude plugin marketplace add ./.dist

A trimmed component is not an error. It is a skill whose record type this corpus does not export.

A skill naming no type at all is not reached by that trim, and who it serves decides what happens to it instead. corpus-retrieval reaches the published source for whichever lookup skills are left, and request-deviation asks the owner of a clause a lookup found, so both follow the last lookup out. raise-finding is declared standalone and ships whatever the corpus adopted. A corpus with no records is the one a session most needs a route to report.

An install

claude plugin marketplace add ./.dist

Both directories are untracked, so nothing here needs a branch or a credential. What you get on a laptop is exactly what CI publishes.

The components that shipped

A corpus whose export includes three record types ships a lookup skill for each, beside the three that name no type of their own. Adoption is not the test. example-engineering adopted controls and has no record of one, so that skill is trimmed alongside the type it never carried. The closing line counts what survived:

bundle: wrote 57 file(s) to .dist/plugin/ as example-engineering 0.19.0. 7 component(s) included, 3 trimmed.

bundle.json lists them, and it travels inside the plugin:

jq -c '{kept: [.included[].path], trimmed: [.trimmed[].path]}' .dist/plugin/bundle.json
{"kept":["skills/corpus-retrieval","skills/glossary-lookup","skills/raise-finding","skills/request-deviation","skills/policy-lookup","skills/standards-lookup","hooks"],"trimmed":["skills/controls-lookup","skills/fix-lookup","skills/process-lookup"]}

To see which assembled skill reads the file one type exports, search for it:

grep -l clauses.jsonl .dist/plugin/skills/*/SKILL.md
.dist/plugin/skills/policy-lookup/SKILL.md

The plugin bundle says what a component declares, and what decides whether it travels.

A pipeline check

npm install -g @anthropic-ai/claude-code
claude plugin validate ./.dist/plugin --strict
claude plugin validate ./.dist --strict

bundle validates nothing it assembles, so this is the layer that does.

The plugin manifest

.plugin/.claude-plugin/plugin.json is where a corpus declares what its plugin ships: metadata.corpusRoot, and the components under metadata.components. Who the plugin is comes from .corpus.yaml instead, through the export, and bundle writes it into the manifest that travels.

Manifest key Comes from
name corpus
version content-version
displayName, description, license, author the four keys beside them
homepage, repository publishing.base
keywords the types the export carried

bundle removes a key the corpus declared nothing for. It does not leave the key standing. A plugin manifest copied from a template names somebody, licenses something and points at a repository. Every corpus copying that file would publish under an identity it never chose.

author is the exception, because the format asks for one and claude plugin validate --strict fails a manifest with none. A corpus that named nobody is filed under its own name, which says the corpus wrote its own plugin and says nothing about a person. pack files a package's authors the same way. Nothing asks for license, so a corpus that chose none asserts none.

Every other key survives untouched, including one this tool has never heard of. What a corpus adds to its own manifest is its own.

Known limits

It validates nothing it assembles. A component misplaced inside .claude-plugin/ leaves here unreported. claude plugin validate runs one layer out, which keeps the build runnable without the Claude Code CLI installed.

It does not publish. Pushing the result anywhere is a separate job, and one that needs credentials this one should not have.

The hook has been proved on macOS only. Nothing yet says which shell Claude Code reaches a hook command with on Windows, so nothing yet says whether the .cmd half of the pair is ever the one that runs. The round-trip test installs the plugin on a Windows runner but opens no session, so it cannot answer this.

A component's requires is not checked against the schema. bundle trims a component that requires a type no schema declares, with the same message as one that requires a type this corpus declined. One is a typo and the other is a decision, and nothing reports the first.

The export is copied whole. A component that survives the trim pulls in the entire export, including types no surviving component requires. That costs nothing while the trim and the export are driven by the same adoption. It is worth reopening for a corpus that exports many types where a plugin reads one.

export is what writes the data this assembles.