A Claude Code plugin is a bundle of harness extensions: skills, slash commands, sub-agents, lifecycle hooks, MCP servers, and executables. Every developer who wants one today either clones a repo by hand or trusts a public marketplace. AgentRegistry gives you a third option, which is the one a platform team actually wants: the plugin becomes a governed catalogue entry, pinned to an exact commit, with a full inventory of what it will run on a laptop.
This page walks the whole loop. Author a bundle locally, push it to git, register the pointer with arctl, watch the controller pin and scan it, then install it into Claude Code from the registry's own marketplace URL served through agentgateway. Everything below was run end to end on a kind cluster.
Preview. The Plugin kind and the marketplace compatibility endpoint are newer than the current release, so this ran on a development build of AgentRegistry Enterprise. Treat the field names as settled and the packaging as ahead of the release train.
The registry holds a pointer, not the bytes
The first thing to understand about Plugin is what it does not do. AgentRegistry does not store the bundle. The spec is a pinned pointer at an external source, which is the same model Agent and Skill already use, and the reason is that the alternative makes the registry a binary artifact store: storage to run and secure, upload idempotency, garbage collection of orphans, and a decision about what happens when storage is not configured.
So the spec is user intent only, and everything the server works out for itself lands in status:
spec (you write this)
title, description, iconUrl, harnesses, and a source. For a git source that is a repository URL plus at most one of branch or a full 40-character commit, with an optional subfolder for a monorepo.
status (the controller writes this)
resolvedSource with the concrete commit the ref pinned to, manifest parsed from the bundle's plugin.json, and inventory, which is a scan of the files the bundle actually ships rather than a repeat of what the author claimed.
Keeping the resolution out of the spec has a practical payoff: a status write never changes the spec's content hash, so re-applying identical intent is a no-op rather than a new revision.
What goes in the bundle
The bundle is a normal Claude Code plugin. The layout is what the registry scans, so it is worth being deliberate about it: each directory below maps onto one line of the inventory the registry will show your security reviewer.
| Path | Inventory field | What it means for a reviewer |
|---|---|---|
.claude-plugin/plugin.json | manifest | Name, version, description. The version becomes the marketplace entry's version. |
skills/<name>/SKILL.md | skills | Name and description read from the frontmatter, not from the manifest. |
commands/<name>.md | commands | Slash commands the plugin adds to the session. |
agents/<name>.md | agents | Sub-agents. Markdown prompt files, not manifest entries. |
hooks/hooks.json | hooks | Lifecycle event and handler type. This is arbitrary code on a developer machine. |
.mcp.json | mcpServers | MCP servers the harness will connect to once the plugin is enabled. |
bin/<name> | executables | Shipped binaries or scripts. Also arbitrary code. |
The plugin used here reviews agentgateway and kgateway policy YAML. Its manifest:
{
"name": "agentgateway-policy-review",
"version": "0.3.0",
"description": "Reviews agentgateway and kgateway policy YAML before it reaches a cluster: checks every document carries apiVersion and kind, flags templating that the CEL engine will not evaluate, and warns on a kubectl apply of an unreviewed policy file.",
"author": {
"name": "Solo.io field engineering"
}
}
It ships a hook so there is something with real teeth in the inventory. The hook runs on every Bash call, but only acts on a kubectl apply -f whose file fails the bundled linter, in which case it exits 2 and the tool call never runs:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/bin/guard-apply"
}
]
}
]
}
}
Registering the pointer
Push the bundle to a repository whose root is the bundle root, then hand the registry a pointer to it. Only github.com resolves today, and the clone runs with whatever credentials the registry pod carries, so a private repository needs the chart's git credential wired in.
apiVersion: ar.dev/v1alpha1
kind: Plugin
metadata:
name: agentgateway-policy-review
namespace: default
spec:
title: agentgateway policy review
description: >-
Reviews agentgateway and kgateway policy YAML before it reaches a cluster.
Ships a skill, a slash command, a read-only auditor sub-agent, a PreToolUse
hook, two executables, and an MCP server pointing at the registry catalogue
through the gateway.
harnesses:
- claude-code
source:
type: git
git:
repository:
url: https://github.com/tjorourke/agentgateway-policy-review
branch: main
arctl apply -f yaml/plugin.yaml
Note what is missing from that spec. No version, no skill list, no hook declaration. You declare where the bundle is; the registry works out what it contains. Four things happen next, out of band of your write:
- The controller resolves the ref with
git ls-remote, no clone, and records the concrete commit SHA. - It shallow-clones that exact commit and reads the tree into memory, with ceilings on file count and total bytes so a hostile repository cannot exhaust the controller.
- It parses
.claude-plugin/plugin.jsoninto the typed manifest. - It scans the files into the inventory, then sets
Ready=Truewith reasonResolved.
Failures are classified rather than retried blindly. A ref that does not exist and a git host that is not supported are terminal; a network blip is retryable.
The inventory is the governance surface
This is the part worth showing a security reviewer, because it is derived from the bundle rather than asserted by its author:
arctl get plugin agentgateway-policy-review -o yaml
status:
conditions:
- lastTransitionTime: "2026-08-07T13:48:24.700154442Z"
reason: Resolved
status: "True"
type: Ready
inventory:
agents:
- policy-auditor
commands:
- policy-review
executables:
- guard-apply
- policy-lint
hooks:
- event: PreToolUse
type: command
mcpServers:
- agentregistry-catalog
skills:
- description: Review an agentgateway or kgateway policy YAML file against the
field checklist and report findings with the bundled linter. Read-only, never
applies anything to a cluster.
name: agentgateway-policy-review
manifest:
author:
name: Solo.io field engineering
description: 'Reviews agentgateway and kgateway policy YAML before it reaches
a cluster: checks every document carries apiVersion and kind, flags templating
that the CEL engine will not evaluate, and warns on a kubectl apply of an unreviewed
policy file.'
name: agentgateway-policy-review
version: 0.3.0
resolvedSource:
commit: db6885891222d407c22c1a508e85170d763258de
type: git
Two entries there deserve a policy conversation before anyone installs this: hooks shows a PreToolUse handler of type command, and executables shows two shipped scripts. Both are arbitrary code that will run on a developer's machine. The registry does not stop you shipping them, it makes sure nobody can ship them quietly.
Serving the catalogue as a marketplace
Claude Code installs plugins from a marketplace.json, and it accepts a bare URL to one. AgentRegistry can re-expose its resolved plugins in exactly that shape. The endpoint is read-only and off by default, so turn it on at install:
--set extraEnvVars[0].name=AGENT_REGISTRY_PLUGIN_MARKETPLACE_COMPAT_ENABLED \
--set-string extraEnvVars[0].value=true
That mounts GET /plugin-marketplace/marketplace.json. Set AGENT_REGISTRY_PLUGIN_MARKETPLACE_COMPAT_PATH_PREFIX if you need it under a base path instead of the root.
curl -s http://agentregistry.localtest.me/plugin-marketplace/marketplace.json
{
"$schema": "https://json.schemastore.org/claude-code-marketplace.json",
"name": "agentregistry",
"owner": {
"name": "agentregistry"
},
"plugins": [
{
"name": "default.agentgateway-policy-review",
"source": {
"source": "url",
"url": "https://github.com/tjorourke/agentgateway-policy-review",
"sha": "db6885891222d407c22c1a508e85170d763258de"
},
"description": "Reviews agentgateway and kgateway policy YAML before it reaches a cluster: checks every document carries apiVersion and kind, flags templating that the CEL engine will not evaluate, and warns on a kubectl apply of an unreviewed policy file.",
"version": "0.3.0"
}
]
}
Four details in that document matter:
- The entry name is namespace-qualified with a dot,
default.agentgateway-policy-review, because the marketplace schema forbids a slash in a plugin name and a bare name would collide across namespaces. - The source carries the resolved
sha, not the branch you registered. Claude Code checks out that commit directly, so two developers who install on different days get identical bytes. - A plugin with a
subfoldercomes out as agit-subdirsource instead, which makes Claude Code do a sparse clone of just that directory. - Anything not yet
Ready, or resolved to a source with no representation in this schema, is skipped rather than half-emitted. The document never contains a broken entry.
The endpoint is anonymous by design: it is registered as a public path, so a request arrives with a public session rather than credentials, and the enterprise build then scopes what that public session may see through the same list filter the native read path uses.
Where agentgateway sits in this
It is worth being precise about which leg of this flow goes through the gateway, because it is not all of them.
Steps 2, 4 and 6 pass through agentgateway, which is where a policy can sit. Step 5 does not: Claude Code clones the bundle from its origin with the developer's own git credentials, so the gateway never sees those bytes. That is a feature for bandwidth and a constraint for anyone who expected the registry to be the single egress point.
The gateway earns its place on the catalogue leg and on the MCP leg. The plugin here ships an .mcp.json pointing at the registry's own MCP bridge through the gateway, so installing the plugin is also what wires the harness to that endpoint. Claude Code registers it under the plugin's name, plugin:agentgateway-policy-review:agentregistry-catalog, and the endpoint is protected: an unauthenticated request gets a challenge naming where to authenticate, so the harness has to hold a registry identity before the entry will serve tools.
curl -s -i http://mcp.localtest.me/mcp | grep -i www-authenticate
Www-Authenticate: Bearer resource_metadata="http://agentregistry.localtest.me/.well-known/oauth-protected-resource/mcp"
That is the shape worth planning for. A bundle can carry MCP wiring, and the endpoint it names can be one your gateway fronts and your identity provider gates, so a developer installing a plugin does not also get an unauthenticated path to your tools.
Installing from Claude Code
Two commands on the laptop. The marketplace URL is the registry's, and the plugin name is the namespace-qualified one from the catalogue:
claude plugin marketplace add http://agentregistry.localtest.me/plugin-marketplace/marketplace.json
claude plugin install default.agentgateway-policy-review@agentregistry
Use --scope local on both if you want the marketplace and the plugin confined to one project directory rather than declared for your user.
The installed cache is keyed by the manifest version, and the executable bits survive the round trip, which matters for a bundle that ships bin/ entries:
~/.claude/plugins/cache/agentregistry/default-agentgateway-policy-review/0.3.0/
├── agents/policy-auditor.md
├── bin/guard-apply
├── bin/policy-lint
├── commands/policy-review.md
├── hooks/hooks.json
└── skills/agentgateway-policy-review/SKILL.md
The slash command is namespaced by the plugin name, so it is the qualified name again:
claude -p "/default.agentgateway-policy-review:policy-review /tmp/policy-broken.yaml"
agentgateway-policy-review · policy-lint 0.3.0
file: /tmp/policy-broken.yaml
document 1 (line 1)
FAIL envelope document is a fragment: add apiVersion and kind
FAIL expressions {{ jwt.team }} is carried through literally; write it as a CEL expression
PASS apiGroup no apiVersion to check
PASS targetRefs not a policy document, no target needed
RESULT: FAIL (2 checks failed)
And the hook fires on the tool call rather than on the model's intent, so it stops the apply before kubectl is invoked at all:
guard-apply blocked this apply: /tmp/policy-broken.yaml does not pass
agentgateway-policy-review.
Moving the pin
A branch in the spec is resolved once, at reconcile, and the commit it resolved to is then immutable in status. Pushing new commits to that branch does not silently move what the catalogue serves. The controller is level-triggered on generation, so a resync re-lists plugins and enqueues the ones whose status is behind their spec.
That is the right default for a catalogue, and it means an update is a deliberate act:
- Change the bundle, bump the version in
plugin.json, push. - Change the
Pluginspec and apply it, or publish it under a new tag. - The controller re-resolves, and
marketplace.jsonstarts serving the newshaand version. - On the laptop,
claude plugin marketplace update agentregistrythenclaude plugin update <name>@agentregistry.
✔ Plugin "default.agentgateway-policy-review" updated from 0.2.0 to 0.3.0
The old version stays in the cache alongside the new one, so a rollback is a version change rather than a re-fetch.
If you want a plugin that can never move, put a full 40-character commit in the spec instead of a branch. A short SHA is rejected, because it would never resolve and would retry forever.
What to take from this
Plugins in AgentRegistry, the short version
- The spec is a pointer. Push the bundle to git, register the URL and a ref, and let the controller pin the commit.
- Read
status.inventorybefore you approve anything. It is scanned from the bundle, not copied from the author's manifest. - Treat
hooksandexecutablesin the inventory as the two lines that need a human decision. Both run arbitrary code on a developer machine. - Turn the marketplace endpoint on deliberately with
AGENT_REGISTRY_PLUGIN_MARKETPLACE_COMPAT_ENABLED. It is anonymous by design, and off until you say otherwise. - Put the catalogue URL behind agentgateway. That is the leg a policy can sit on, along with any MCP server the bundle declares.
- Remember that the bundle bytes come from the origin, cloned with the developer's own git credentials, not through the registry.
- Pin with a full commit SHA when a plugin must never move. Use a branch plus a deliberate re-apply when you want a promotion step.