Packaging so a new machine is one install away
Turn the skills and hooks living in one laptop's home directory into a versioned repo a new machine installs from a git remote — or a teammate gets by cloning.
The setup that only exists on one laptop
So you already have a setup — /goals command forcing the agent to declare its intent before starting work on a feature, /review command which the team actually uses, maybe even a commit hook that doesn't let you do some things; all of it in your home directory. But this setup is far from perfect, there are four problems with it:
if you get a new machine — you'll need to recreate it based on memory and will forget about sth
if a teammate tries to /review — you'll zip it for them but won't send them the fix of a bug you've found later
if you have a second repo — you'll clone your home directory there, from now on two versions of the setup will evolve in separate places
and whenever somebody asks which version everyone is on — you'll have no answer, because nothing is versioned
So it's not that you're missing a tool. Every harness has a way to install a custom bundle from a git remote (except one, but even in that case it can read a directory which you can version using git). It's just that you already have a setup on the machine in front of you.
Dotfiles territory, you might say — there's a symlink script that does it
I'd say — for one person, for some time. And then it stops working at four different levels:
versions — you can't say "we're on 0.3", you can't revert to yesterday's version, you don't know if the bug somebody found is in their version of your symlink script
paths — every harness expects a bundle in a different place and the differences actually matter (one requires the manifest at the plugin root level, another one can't find it unless it's in a dot-directory), so for each tool you need to create a new branch in the script
scope — symlinks are binary, most of the harnesses come with an option to set up the install scope so a bundle is enabled on one project and disabled everywhere else (you'll need this as soon as you have a skill that's needed in one repo and not needed in others)
the clone (which is what makes all the difference) — in order to set up your symlink script you need to run it, but if somebody clones your repo they don't run it; they clone and start the agent and there's nothing
So we can change the goal, from installing sth to installing nothing. One install is good, zero installs is available — and it's the same amount of work.
Three nouns, and only three
Let's start by setting up the vocabulary, because there are three terms which almost every tool uses and they're nearly identical once you line them up:
component — a single thing that does sth (skill directory with SKILL.md, an agent, a hook config, MCP server entry or LSP server entry)
plugin — a folder of components with a manifest in which you list and version them; it's what people install
marketplace — a file with the catalogue of plugins with their locations; it's what people subscribe to
In theory you can distribute a plugin without a marketplace but I recommend having one anyway, even for a single plugin. Because installing directly from a repo path is being deprecated — look, the Copilot CLI 1.0.77 already prints Direct plugin installs (repos, URLs, local paths) are deprecated. Only plugin@marketplace installs will be supported in a future release. when you do it. The catalogue file is about fifteen lines long, and it's the difference between sth you can hand somebody and sth you have to explain to them.
The repo
So let's create a repo with a nested directory inside (for the plugin) and put a marketplace file there which will point to it. I'll show you the marketplace file (which we're going to create today as part of this lesson), and also the manifest for the plugin itself:
team-kit/
├── .claude-plugin/
│ └── marketplace.json # the catalog: which plugins live here
└── plugins/
└── team-kit/
├── .claude-plugin/
│ └── plugin.json # the plugin's own manifest
├── plugin.json # the same content again, at the root
└── skills/
└── goals/
└── SKILL.md # the skill you already wrote{
"name": "team-kit",
"owner": { "name": "Acme Engineering" },
"metadata": {
"description": "Acme's shared agent setup: one install on a new machine.",
"version": "0.1.0"
},
"plugins": [
{
"name": "team-kit",
"source": "./plugins/team-kit",
"description": "The skills, agents and hooks our team expects on every machine."
}
]
}{
"name": "team-kit",
"version": "0.1.0",
"description": "The skills, agents and hooks our team expects on every machine.",
"author": { "name": "Acme Engineering" }
}The thing is, Claude Code 2.1.220 doesn't like it when a plugin has its manifest only at the root level — it says No manifest found in directory. Expected .claude-plugin/marketplace.json or .claude-plugin/plugin.json — and Antigravity CLI 1.1.9 doesn't like it when the manifest isn't at the root level, it says missing plugin.json. Neither of them checks where the other one looks, and both are fine with the same file content, so we just duplicate six lines and that's the cheapest way to make it work. Copy one into the other in whatever check you already run, so they can never get out of sync.
One repo for multiple harnesses, seriously
Worth knowing before you decide how much to build, because it's less work than you might think.
Claude Code and Codex read the same catalogue file. I pointed Codex CLI 0.146.0 at the repo above — untouched, nothing renamed — and it resolved that repo's .claude-plugin/marketplace.json, printed the path it had resolved, and installed from it. Copilot CLI installs the same plugin directory too, and .claude-plugin/plugin.json is one of the locations it looks for a manifest in.
If you need a different manifest or a different way to install a bundle — there are a few more harnesses:
Cursor, which looks for .cursor-plugin/
Gemini CLI, which doesn't have any plugins at all but extensions instead, with a separate manifest and a separate command to install
and one which doesn't even come with a way to install a bundle, it can just read directories
The variant below has the shape for whichever one you're on.
What not to put in it
Once you can install sth, you can distribute it. So the standards are higher:
don't include your credentials in hook scripts, MCP server entries or env defaults — bundles might end up on machines you don't control
don't put absolute paths from your own machine in there (sth like
/Users/you/work/scripts/lint.shis the most common reason why a bundle stops working for anyone but its author)don't add all your experiments to it — keep them in your home directory
The last one has a measurable cost and not just a tidiness argument. claude plugin details prints a projected token bill for what you're shipping; for the single-skill plugin above it reported Always-on: ~25 tok added to every session, with the skill itself around 30 tokens always-on and another 40 per activation. Those numbers are estimates and the tool says so — but look at what the always-on half actually is: the part it adds to every session whether or not the skill ever fires. So 40 skills nobody uses is not free.
In Claude Code
Plugins and marketplaces, all of it available through the CLI so nothing here needs an interactive session.
Two different locations where the configuration goes:
in the root of the repository, a
.claude-plugin/marketplace.jsonfile that holds the marketplacein each plugin directory, a
.claude-plugin/plugin.jsonwith the configuration for that plugin
Validate before you push. The --strict flag makes the tool treat warnings as errors and exit non-zero, which is what you want in CI — and validate the repo and the plugin directory separately:
claude plugin validate . --strict
claude plugin validate ./plugins/team-kit --strictTo distribute it, push to a remote and then:
claude plugin marketplace add acme/team-kit
claude plugin install team-kit@team-kitmarketplace add takes a GitHub owner/repo, a URL or a path on disk, and if your marketplace is part of a bigger monorepo the --sparse flag limits the checkout to specific directories.
Both commands take --scope user|project|local, which decides whether the entry lands in your own settings or in the repo's. project is the one that writes into the repo — you commit that file, and then nobody has to install anything.
To see what arrived:
claude plugin list
claude plugin details team-kitThe most useful one is details. It lists the components and gives you an estimate of the tokens the bundle costs, which tells you what one session with it is actually going to cost you.
I'd say the common mistake is putting the marketplace description at the top level, next to name, instead of inside metadata. If you do that, the validator still complains with No marketplace description provided and --strict exits non-zero. Move the description into metadata and it passes. It's confusing because the error never says where the field is supposed to go, so it looks like a bug in the validator.
In Codex CLI
Codex has the same plugin and marketplace design as Claude Code, and it uses the very same marketplace.json in the .claude-plugin folder. So if you already built the repo for Claude Code you just point Codex at it — no second manifest, nothing renamed. That's Codex 0.146.0 reading a Claude Code repo as-is.
One thing to know first: the subcommand is add, not install. codex plugin install comes back with unrecognized subcommand 'install', which is a confusing thirty seconds if you're arriving from the other tool.
It's as simple as this:
codex plugin marketplace add acme/team-kit
codex plugin add team-kit@team-kit
codex plugin listYou can point at the marketplace in any of these ways:
a path on disk
owner/repo (and optionally a ref)
a git URL over HTTPS
a git URL over SSH
with
--ref <REF>to pin a revisionwith
--sparse <PATH>in case your marketplace sits in a monorepo
The quickest way to see which file was actually loaded is codex plugin list — it prints the resolved path to the marketplace manifest, which is super handy.
The config lives in ~/.codex/config.toml, and installing populates it for you with the marketplace source and the plugin:
[marketplaces.team-kit]
source_type = "local"
source = "/path/to/team-kit"
[plugins."team-kit@team-kit"]
enabled = trueThe binary also carries strings for .codex-plugin/plugin.json and Cursor's .cursor-plugin/, so other layouts probably resolve too — but the one above is the one we actually drove. Don't add layouts speculatively; start with the single catalogue file, confirm codex plugin list finds it, and only split when a specific harness refuses.
In GitHub Copilot CLI
Here the plugin.json goes in the top level directory of your plugin, not in a dot-directory. That's the opposite of what Claude Code wants, and it's exactly why the repo above ships that file in both places.
The structure of the plugin is as follows:
plugin.jsonskills/agents/hooks.json.mcp.jsonlsp.json
By default there are two marketplaces configured:
github/copilot-plugins
github/awesome-copilot
So you can already use the browse command to explore them:
copilot plugin marketplace browse copilot-pluginsIf you want to add your own marketplace you need to have it on GitHub. The marketplace is identified by owner and repository, so that's what you pass to plugin marketplace add. Point it at a local path instead and it'll try to clone https://github.com/./team-kit.git — there's no local-path escape hatch here, you publish to GitHub first.
For example:
copilot plugin marketplace add acme/team-kit
copilot plugin install team-kit@team-kit
copilot plugin list
copilot skill listRun the skill listing as well as the plugin one. In the list of plugins you only see the name of the bundle; in the list of skills you see the components inside it, so that's how you make sure the thing you wanted is actually loaded and not just the wrapper around it.
To see a plugin on the list without installing it, point the CLI at the plugin directory with the --plugin-dir flag:
copilot plugin list --plugin-dir ./plugins/team-kitBut don't build on the direct-install route. Installing from a local path, a URL or a repo still works today, and each of those prints GitHub's own deprecation notice saying that only the plugin@marketplace format will be supported in a future release. The declarative equivalent is an enabledPlugins entry in .github/copilot/settings.json for a repository, or ~/.copilot/settings.json for yourself.
In Cursor
Cursor expects the manifest at .cursor-plugin/plugin.json. For a repo hosting multiple plugins you put a .cursor-plugin/marketplace.json at the top level — same shape as the catalogue above, just a different directory. The only field it insists on is name, in lowercase kebab-case.
A minimal plugin manifest:
{
"name": "team-kit",
"displayName": "Team Kit",
"description": "The skills, agents and hooks our team expects on every machine.",
"version": "0.1.0"
}When you don't give a path in the manifest, Cursor finds the components by convention:
skills/ — a subdirectory per skill, with SKILL.md in it
rules/
agents/
commands/
hooks/hooks.json
mcp.json
So if you built the plugin for a different harness it's usually enough to add the Cursor manifest, not to rearrange anything.
But watch this: if you do put an explicit path in the manifest for one of the component types, it's used instead of the convention, not on top of it. Point skills at ./my-skills/ and Cursor stops scanning skills/ — the skills that were working a minute ago simply stop being found.
The other thing worth knowing is that a marketplace here is attached to your account, not to a directory. You add one by pointing at a git URL:
agent plugin marketplace add <git-url>and list the ones your account can see:
agent plugin marketplace listThat's the reminder that this isn't per-repo state — push a change to the marketplace repo, re-index it with agent plugin marketplace update, and it lands for everyone on it.
During development you can point a session straight at a plugin directory instead:
agent --plugin-dir ./plugins/team-kitIn Gemini CLI and Antigravity CLI
In terms of the packaging standard these are completely different worlds, so work out which one you're on first.
Gemini CLI doesn't have plugins at all — it has extensions. The manifest is gemini-extension.json at the repo root, and gemini extensions new ./team-kit scaffolds it with exactly two fields, name and version. Here it is with a description and a context file added on top:
{
"name": "team-kit",
"version": "0.1.0",
"description": "Acme's shared agent setup.",
"contextFileName": "GEMINI.md"
}An extension is one bundle: commands as commands/*.toml, plus the context file the manifest names. The lifecycle is its own too:
gemini extensions validate ./team-kit
gemini extensions link ./team-kit # dev: edits to the path are always reflected
gemini extensions install <git-url> # real installs
gemini extensions list
gemini extensions update --allAnd here the gotcha lands exactly where packaging was supposed to pay off: installing from a local path stops on an interactive prompt asking Do you trust the files in this folder?, so it isn't scriptable on a fresh machine. A file:// URL is no way around it either — that comes back Install source not found. Plan on a real git remote and somebody at the keyboard for the first install.
Antigravity CLI does use plugins, and its manifest is plugin.json in the plugin's root. The Claude Code layout isn't valid for Antigravity, and the other way round.
agy plugin validate walks the components (skills, agents, commands, mcpServers, hooks) and tells you how many were processed and which weren't found, so you can be sure everything is in place before a release:
agy plugin validate ./plugins/team-kit[ok] ./plugins/team-kit
✔ skills : 1 processed
- agents : skipped (not found)
- commands : skipped (not found)
- mcpServers : skipped (not found)
- hooks : skipped (not found)Keep your eyes on the skipped (not found) lines rather than the [ok] at the top — put a component in the wrong folder and it's reported as not found while the plugin as a whole still passes. That's precisely the version of this you'd otherwise ship.
agy plugin install points at a directory and also takes the plugin@marketplace format, and agy plugin import pulls plugins over from an existing Gemini or Claude setup, which is usually less work than a rewrite.
In Kimi Code CLI
Kimi Code has no plugin or marketplace subcommand — there's no target to install into. That lowers the ceiling and the effort at the same time: the work is putting a folder on the machine, and a git repo with git clone is all it takes.
The folder that works is the brand-agnostic one. On Kimi Code 0.31.1, with no Kimi-branded skills folder present at all, a session was reading skills out of ~/.agents/skills/. That's the same location Copilot CLI names as a personal skill source, and probably the one whatever you install next will read too. So it's the same repo again — you just clone it where the tool already looks:
git clone [email protected]:acme/team-kit.git ~/src/team-kit
cp -R ~/src/team-kit/plugins/team-kit/skills/. ~/.agents/skills/That's a copy though, so everything said above about copies applies here — no version to report, no way to upgrade. Treat it as the fallback and prefer the next one where you can.
For anything you don't want resting on discovery order, name the directory explicitly with --skills-dir. The flag can be used multiple times, and it replaces the auto-discovered user and project directories rather than adding to them, which is exactly what CI or a shared script wants:
kimi --skills-dir ~/src/team-kit/plugins/team-kit/skills -p "run the review skill"Config files have their own validator, and it lists every file it checked. Worth running after any edit, because nothing else in the session is going to tell you a config was ignored:
kimi doctorOK config.toml ~/.kimi-code/config.toml
OK tui.toml ~/.kimi-code/tui.tomlTwo things to watch at the end:
discovery is layered across brand-named and generic directories, so the resolution isn't transparent. If a skill is missing, point at it with
--skills-dir— that tells you whether it's a wrong path or a broken file.check which binary you're driving before you trust any of this. The rebuilt Kimi Code CLI reports
0.31.1and keeps its config in~/.kimi-code/, while the retired Python kimi-cli is on a 1.x version. Two separate tools with the same command name.
Check it actually landed
Packaging fails quietly. If you put the manifest in a wrong place nothing breaks — your bundle just isn't there, and you find it out when a teammate says they can't see the skill on their machine. So run your harness's list command immediately after installing and check that the specific component is in the output, not just the plugin. And then put the validator in CI, so that a broken manifest fails on the branch instead of on somebody's laptop.
Just beware though that Claude Code 2.1.220 validates the marketplace file without validating the plugins it points at. I removed the plugin's entire .claude-plugin directory, ran claude plugin validate . --strict against the marketplace, and it still came back Validation passed. So add a validation step for every plugin path as well, otherwise CI will keep telling you the repo is fine while it ships nothing.
Versioning, and the update nobody gets
If you can install sth — you should version it. The version field in the manifest is not decorative; increment it with every change and tag the commit, so that later when somebody comes to you and says "the deploy skill doesn't work on my machine" you can ask them which version they have and point at a specific commit. Without it you're looking at the last five edits and guessing which one they're on.
Then decide, on purpose, whether installs float or pin. If they float, everyone gets your fix with their next update — and everyone also gets the results of the bad afternoon you had. If you pin to a tag or a ref, it's you who decides when the team moves. For a small team I'd float it but tag anyway, so there's always a known-good ref to send sb back to. If there's a compliance story, pin.
Either way it's on you now. A bundle nobody updates quietly rots, and "install once" turns into "install once, in March".
The version that needs no install at all
Claude Code, Codex and Copilot each have a settings file that can name a marketplace and an enabled plugin declaratively. Commit it to the repo and the clone does the installing — nobody types a command.
And you don't even need to author it. Installing the plugin at project scope made Claude Code write the project's own .claude/settings.json, and this is the shape it produced, with the local source directory standing in for the remote you'd really point at:
{
"extraKnownMarketplaces": {
"team-kit": {
"source": { "source": "directory", "path": "/Users/you/src/team-kit" }
}
},
"enabledPlugins": { "team-kit@team-kit": true }
}So replace that path with the actual remote, commit the file, and every new team member's clone is supposed to come with the team's skills already in it. That's the finish line worth aiming at, and the reason to package rather than to keep a good symlink script. But before you commit it — think about:
what if they don't want your bundle? A marketplace entry in a shared repo is a way of saying "trust me", so make sure it's as reviewable as everything else in the tree
what if they have different preferences? Those are personal and belong in their own settings, which is exactly what the separate scopes are for
When this is over-engineering
One skill, one machine, one repo — a directory is the right answer there and a manifest is just ceremony.
The line isn't size, it's copies: the moment the same procedure exists in two places, one of them is already stale and you just haven't found out yet.
So once it's on a second machine, or for a second person, or in a second repo — package it. Until then, keep writing skills.
.claude-plugin/marketplace.json{
"name": "team-kit",
"owner": {
"name": "Acme Engineering"
},
"metadata": {
"description": "Acme's shared agent setup: one install on a new machine.",
"version": "0.1.0"
},
"plugins": [
{
"name": "team-kit",
"source": "./plugins/team-kit",
"description": "The skills, agents and hooks our team expects on every machine."
}
]
}