
Security News
Cloudflare Introduces a Faster TypeScript Compiler Written in Rust
Cloudflare introduces tsrs, a Rust port of TypeScript’s compiler promising faster checks and lower memory use, with the TypeScript team’s help.
@gleanwork/pluginpack
Advanced tools
Compile one source of agent plugins — skills, commands, hooks, and MCP servers — into the native plugin format each AI app expects.
One source of truth for agent plugins across AI app ecosystems.
pluginpack is a build tool for compiling portable skills, commands, agents, rules, hooks, assets, and metadata into the native plugin layouts expected by each AI app. It copies files, writes target manifests, and validates generated output; it is not a package manager or publisher.
Start with portable plugin components, declare the native targets you want, then run pluginpack build.
npm install -D @gleanwork/pluginpack
Create one shared source directory per plugin:
shared/
acme/
skills/
release-notes/
SKILL.md
agents/
search-assistant.agent.md
pluginpack.config.ts
Create shared/acme/skills/release-notes/SKILL.md:
---
name: release-notes
description: Draft release notes from a list of changes.
---
# Release notes
Group the changes by feature and fix. Write a short note for each group.
Create shared/acme/agents/search-assistant.agent.md. Copilot requires the
NAME.agent.md filename; the other targets accept it too:
---
name: search-assistant
description: Find source documents that answer a question.
---
Search for relevant documents and cite the sources in your answer.
Map that shared source directly into each native plugin output. Add overrides
only when a target needs files that differ from or do not exist in the shared
source.
import { defineConfig } from "@gleanwork/pluginpack";
export default defineConfig({
name: "acme-plugins",
version: "0.1.0",
metadata: {
description: "Acme agent plugins.",
author: { name: "Acme" },
license: "MIT",
},
targets: {
cursor: {
outDir: ".",
plugins: {
acme: {
source: "shared/acme",
path: "plugins/cursor/acme",
},
},
},
claude: {
outDir: ".",
pluginRoot: "plugins/claude",
plugins: {
acme: { source: "shared/acme" },
},
},
antigravity: {
outDir: "plugins/antigravity",
plugins: {
acme: { source: "shared/acme" },
},
},
copilot: {
outDir: "plugins/copilot",
plugins: {
acme: { source: "shared/acme" },
},
},
codex: {
outDir: "plugins/codex",
plugins: {
acme: {
source: "shared/acme",
entry: {
policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
category: "Productivity",
},
},
},
},
},
});
Build and validate every configured target:
npx pluginpack build
npx pluginpack validate --target cursor
npx pluginpack validate --target claude
npx pluginpack validate --target antigravity
npx pluginpack validate --target copilot
npx pluginpack validate --target codex
Claude, Cursor, Antigravity, Copilot, and Codex users install from the generated
native layout. Repositories that also expose a skills CLI surface may point
that tool at shared/<plugin>/skills.
Agent apps increasingly support similar ideas: skills, commands, agents, rules, hooks, MCP configuration, and plugin marketplaces. The packaging formats are different enough that maintaining one repo per app quickly drifts.
pluginpack does four things:
It does not try to make every app behave the same. Target adapters own target-specific layout, manifests, and validation.
The preferred authored shape separates shared plugin content, target overrides, and generated-repository files:
shared/
acme/
skills/
agents/
assets/
mcp/
config.json
pluginpack.json
overrides/
cursor/
acme/
rules/
repositories/
cursor/
README.md
pluginpack.config.ts
.cursor-plugin/
marketplace.json
plugins/
cursor/
acme/
.cursor-plugin/plugin.json
agents/
rules/
hooks/
skills/
claude/
acme/
.claude-plugin/plugin.json
agents/
hooks/
skills/
antigravity/
.pluginpack/
antigravity.json
acme/
mcp_config.json
plugin.json
agents/
rules/
hooks/
skills/
copilot/
.claude-plugin/
marketplace.json
.github/
plugin/
marketplace.json
.pluginpack/
copilot.json
plugins/
acme/
agents/
hooks/
skills/
.claude-plugin/
marketplace.json
.pluginpack/
cursor.json
claude.json
Each emitted plugin names one source. Optional overrides are applied after
the source, so it can add or replace files for that target. repositoryFiles
copies a whole directory into the generated repository root.
pluginpack writes a .pluginpack/<target>.json managed-file manifest for each built target. That manifest lets builds and cleanup commands remove stale generated files without touching source files or unmanaged repo content. It also records a sha256 of each file as written. If you edit a generated file and a later build, prune or clean would overwrite or delete it, pluginpack prints a warning naming the file first. It still goes ahead, because the output belongs to pluginpack; move the edit into the source to keep it. Manifests from 0.11 and earlier have no hashes, so they produce no warnings until the next build rewrites them.
Before it deletes anything, pluginpack compares real paths, with symlinks resolved and, on a case-insensitive filesystem, case folded. It refuses an outDir that lands inside a source, overrides, partials, repositoryFiles or rootFiles path, even through a symlink. It also refuses two targets that write the same file through different spellings, such as out and OUT on macOS, or through a generated file that is a symlink to another target's file. Under the default plugins/ root, a directory with a component directory counts as legacy source and is never deleted, unless pluginpack recorded writing every file in it. --dry-run runs the same checks, so it fails wherever the real command would.
If an outDir holds a manifest for a target the config no longer builds there, build and prune refuse to run. That happens when a config drops a target, or moves an outDir to another target, such as codex to agent-plugins, without cleaning first. No configured target would prune those files, so they would stay forever. Run pluginpack clean --target <old> to delete them. It finds the old target's manifest in any outDir the config uses, even when that target is no longer configured. A plain clean deletes them too. To keep the files unmanaged instead, delete the manifest.
An outDir outside the project, such as a sibling output repository (../cursor-plugins), works. The guard there knows only that repository's .git directory and the files the manifest lists, so it cannot protect anything else in that repository from a manifest someone edited by hand.
Upgrading from 0.11 (managed files): the first build rewrites each manifest as version 2. pluginpack 0.11 cannot read a version 2 manifest, so upgrade every machine and CI job that builds the same output together. A manifest from a newer pluginpack fails with a message saying to upgrade. Some configs that 0.11 accepted now fail at load or build: source.partials outside the project, an outDir inside a source path, and a rootFiles entry that copies a file onto itself ("README.md": "README.md" with outDir: "."). Each error names the field to change. A build into an outDir that still holds a dropped target's manifest also fails until you run pluginpack clean --target <old>.
In pluginpack, a component is a top-level plugin capability directory. Components are the portable pieces of a source plugin that may or may not exist in every target ecosystem.
Supported component directories are:
skills/
agents/
commands/
rules/
hooks/
scripts/
assets/
policies/
themes/
Target adapters translate those component directories into each app's native layout and manifest fields. Each target has a smart default component list. By default, claude, cursor, antigravity, and copilot emit skills and other native plugin support files but omit commands, since those ecosystems increasingly expose skills as slash commands.
Use include or exclude only when a target needs a different content set:
import { defineConfig } from "@gleanwork/pluginpack";
export default defineConfig({
name: "acme-plugins",
version: "0.1.0",
metadata: {
description: "Acme agent plugins.",
author: { name: "Acme" },
license: "MIT",
},
targets: {
antigravity: {
outDir: "plugins/antigravity",
plugins: {
acme: {
source: "shared/acme",
include: ["skills", "commands", "static"],
},
},
},
claude: {
outDir: "plugins/claude",
plugins: {
acme: { source: "shared/acme", exclude: ["commands"] },
},
},
},
});
Each target compiles the same source into one app's native plugin layout:
| Target | Native format | Output it writes |
|---|---|---|
cursor | Cursor plugin + marketplace | .cursor-plugin/marketplace.json; a .cursor-plugin/plugin.json per plugin |
claude | Claude plugin + marketplace | .claude-plugin/marketplace.json; a .claude-plugin/plugin.json per plugin |
antigravity | Antigravity CLI plugin | a plugin.json per plugin + optional mcp_config.json (no marketplace) |
copilot | GitHub Copilot plugins | .claude-plugin/marketplace.json mirrored to .github/plugin/marketplace.json; plugins under plugins/<name>/ |
codex | OpenAI Codex / ChatGPT plugins | .agents/plugins/marketplace.json; an Agent Plugins package per plugin (plugin.json + optional mcp.json) |
agent-plugins | Agent Plugins 1.0 | a portable package per plugin under plugins/<name>/; no marketplace unless clients is set (one package for several clients) |
Heads up: targets that write the same marketplace file need distinct
outDirs (or separate repos):claudeandcopilotboth write.claude-plugin/marketplace.json;agent-pluginswithclients.codexwrites thecodextarget's.agents/plugins/marketplace.json; and withclients.copilotit writes thecopilottarget's.github/plugin/marketplace.json.builderrors on overlapping output paths. Collision checks normalize Unicode to NFC and ignore case on every filesystem:assets/Logo.pngandassets/logo.pngcollide even on a case-sensitive volume. Rename one file so the output stays portable.
Oracle strength differs by target. See the conformance oracles: Claude uses its CLI validator, Cursor uses Glean-authored schemas, and Codex and Agent Plugins use official package schemas. Codex and Copilot marketplace checks and Antigravity layout checks use structural assertions against product docs or published examples.
New targets are added from official docs or real plugin examples, not guessed abstractions.
Agent Plugins 1.0 is an open
specification for the plugin package itself: a closed plugin.json
manifest, Agent Skills under skills/, MCP servers in mcp.json, and
reverse-domain extension namespaces for anything client-specific.
Marketplaces, hooks, agents, commands, and rules are deliberately outside it.
Pluginpack emits packages in two ways:
codex emits a package per plugin and keeps Codex's
.agents/plugins/marketplace.json. Authored manifest fields that aren't
part of the portable manifest (interface, apps, hooks) move under
extensions["com.openai"] automatically, so existing manifest config needs
no changes. MCP config is held to the stricter Agent Plugins rules (see
MCP Directory). Set format: "legacy" on the target to keep the older
.codex-plugin/plugin.json layout for Codex releases older than 0.147.0,
the first release that installs Agent Plugins packages. format: "legacy"
is deprecated in 0.12 and will be removed in 0.13; a build that uses it
prints a warning (see
ADR 0001).agent-plugins emits portable packages. On its own it has no client
profile, so it ships skills, scripts, assets, static files, and mcp.
Selecting agents, commands, rules, or hooks is a config error, and a
manifest field outside the portable manifest is a build error. It writes no
marketplace, because the specification defines none. List clients to
build one package for several clients (see below).Set clients on the agent-plugins target to build each plugin once and
publish it to every listed client from one output repository. Each package
keeps one plugin.json, mcp.json and skills/. Each client gets its own
marketplace file, and its client-only files go in its extension directory
(ADR 0002):
| Client | Marketplace | Client-only files |
|---|---|---|
codex | .agents/plugins/marketplace.json | none yet |
copilot | .github/plugin/marketplace.json | agents/NAME.md (or .markdown, .mdc) → com.github.copilot/agents/NAME.agent.md; any other file in agents/ fails the build |
targets: {
"agent-plugins": {
outDir: "dist/agent-plugins",
clients: {
codex: { manifest: { interface: { displayName: "Acme" } } },
copilot: {},
},
plugins: {
acme: {
source: "shared/acme",
manifest: { extensions: { "com.openai": { interface: { displayName: "Acme" } } } },
clients: {
codex: {
entry: {
policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
category: "Productivity",
},
},
},
},
},
},
},
clients.<client>.manifest is deep-merged into that client's marketplace
file. plugins.<name>.clients.<client>.entry is deep-merged into the
plugin's entry in it.manifest and per-plugin entry are config errors on
agent-plugins, with or without clients, because the target writes no
shared marketplace for them to go into. Move them to
clients.<client>.manifest and plugins.<name>.clients.<client>.entry.manifest.extensions["<namespace>"]
explicitly. A package for several clients can't guess which client a
top-level field belongs to, so a non-portable top-level field is still a
build error.include, only when pluginpack places it for a listed client. Today that
is agents for copilot. pluginpack doesn't place commands, rules, or
hooks for any client yet. With clients.codex, build warns when it omits
source hooks. Keep the codex target if hooks are required, or explicitly
exclude hooks to acknowledge the omission.metadata.owner or
metadata.author.validate checks every package, then each client marketplace that's
present: every entry must point at a package whose plugin.json has the
same name. Each Copilot agent must be named NAME.agent.md and have the
name and description frontmatter Copilot requires.validate checks packages against the specification: the closed manifest, a
valid plugin name, mcp.json servers, and skill names that follow the
Agent Skills specification and match
their directory. Conforming clients skip a skill that fails these checks.
Build warns about invalid skill frontmatter and missing Codex entry fields;
validate reports them as errors. Supply policy.installation,
policy.authentication, and category in entry (or clients.codex.entry).
Moving between codex and agent-plugins with clients.codex: run
pluginpack clean --target codex while the old target is still configured,
before removing it or building its replacement into the same output directory.
This removes the old managed manifest and marketplace without deleting source
files. In the reverse direction, run pluginpack clean --target agent-plugins
before removing that target. The agent-plugins target does not ship Codex hooks.
0.12 changes generated output for codex, claude, cursor and copilot.
Some configs and sources that 0.11 accepted now fail at config load, build or
validate. MIGRATING_TO_0.12.md lists each error
with its fix, then the changed output, the new warnings and the API type
changes. The first 0.12 build rewrites each .pluginpack/<target>.json in a
format 0.11 can't read, so upgrade every machine that builds the same output
together.
codex target's output layout changes. Run pluginpack build, and
either publish the new layout or set format: "legacy" until 0.13. If
validate reports skill names such as skill_name, rename them before
switching, because Agent Plugins clients skip them. The build can also fail
on MCP config that 0.11 copied as authored, such as cwd: "." or a
${API_TOKEN} placeholder. Change it to follow the Agent Plugins rules in
MCP Directory, or set format: "legacy" to keep writing
.mcp.json as authored.cursor build fails when the shared MCP config uses
${CLAUDE_PLUGIN_DATA} or ${PLUGIN_DATA}, which 0.11 shipped to Cursor
unexpanded:
MCP server "local" uses ${CLAUDE_PLUGIN_DATA}, but Cursor has no plugin-data variable.
The error stops every target, so nothing is written. Give cursor its own
mcp/config.json through overrides, or add "mcp" to its exclude.claude, cursor and copilot no longer copy MCP config as authored. A
./bin/server command becomes ${CLAUDE_PLUGIN_ROOT}/bin/server for
claude, ${CURSOR_PLUGIN_ROOT}/bin/server for cursor and
${PLUGIN_ROOT}/bin/server for copilot. A claude build fails on an MCP
server with cwd, because Claude Code ignores it. Other root and data
variables are renamed to each client's own. "type": "streamable-http"
becomes "type": "http" for claude and copilot, and cursor drops
type for stdio and HTTP servers.validate rejects ---json frontmatter and tab-indented YAML, which 0.11
accepted. An unquoted number with a leading zero, such as 017, now reads
as 17 instead of the octal 15.Pluginpack 0.11 can still read source.plugins, source.skills, rootPlugin,
from, nested targets/<host> replacements, and root .mcp.json files for one
migration window. New repositories should not use that interface. Follow
MIGRATING_TO_0.11.md to convert an existing repo.
Hosts don't reliably tell users a plugin is outdated (Claude Code's auto-update is off by default for third-party marketplaces; Cursor has no nudge at all). Opt in per target with updateCheck and pluginpack generates a session-start hook into each emitted plugin:
targets: {
claude: { outDir: "...", updateCheck: {}, plugins: { ... } },
cursor: { outDir: "...", updateCheck: {}, plugins: { ... } },
}
Each emitted plugin gains scripts/pluginpack-update-check.sh plus a hooks/hooks.json registration (merged into a source-authored hooks/hooks.json when one exists). At session start the script compares the version stamped at build time against the latest stable semver git tag of the plugin repo and, when behind, nudges: on claude via a user-visible systemMessage (with the /plugin update command to run); on cursor via agent context (Cursor's sessionStart hook has no user-visible output).
The check follows update-notifier discipline:
updateCheck.repository, defaulting to metadata.repository (an error if neither is set).git ls-remote per repo per 24h, cached under ${XDG_CACHE_HOME:-~/.cache}/pluginpack/ and shared across plugins from the same repo.git, odd tags, or any other problem exits silently.CI is set or PLUGINPACK_NO_UPDATE_CHECK=1.Disable for a single plugin with updateCheck: false on that plugin. Configuring updateCheck on copilot, antigravity, codex, or agent-plugins is a config error — those hosts don't run plugin hooks.
Like MCP config, the generated hook is wired in regardless of a plugin's content selection: on cursor, the manifest's hooks field is set even if include does not name "hooks", since the check itself is a separate opt-in.
Once a target's output is pushed to a repo, pluginpack install-info prints the real, doc-verified command or URL a user needs to add that marketplace — one per configured target:
pluginpack install-info
pluginpack install-info --target claude
The repo comes from targets.<name>.repository, defaulting to metadata.repository (an error if neither is set) — the same fallback updateCheck.repository uses. Every target today resolves to a real command except cursor and agent-plugins. cursor has no CLI equivalent: it prints the repo URL and a note to paste it into Cursor's Dashboard under Team Marketplaces. agent-plugins has no install snippet, because the specification defines no marketplace or install command. See CONFORMANCE.md's "Install-snippet facts" section for the doc citation behind each target's snippet.
The Claude Code command reference shows both operations as shell commands:
claude plugin marketplace add <repo>
claude plugin install <name>@<marketplace>
install-info prints that shell sequence. Its note retains the interactive
/plugin marketplace add <repo> and /plugin install <name>@<marketplace>
forms. In a session, /plugin install opens the plugin details so you can
review it and choose an install scope.
The Antigravity CLI docs show this exact GitHub URL command:
agy plugin install https://github.com/<owner>/<repo>
install-info substitutes the configured repository URL. The docs also show
agy plugin install </path/to/local/plugin>, but do not specify a GitHub URL
subdirectory selector. To install a generated plugin from a repository
subdirectory, use the clone-then-local-install command in the snippet's note.
That command uses the generated pluginPath, including a custom plugin path.
When one app needs different or additional content, put it in that emitted plugin's target overrides:
shared/acme/skills/release-notes/SKILL.md
overrides/cursor/acme/skills/release-notes/SKILL.md
overrides/cursor/acme/rules/cursor-only.mdc
The overrides are applied after the shared source, so they can both replace the
shared skill and add the Cursor-only rule. Set their directory with the emitted
plugin's overrides field.
An authored plugin keeps its MCP configuration and local server together:
shared/acme/mcp/
config.json
pluginpack.json
start.mjs
dist/index.js
src/
tests/
config.json uses the standard { "mcpServers": { ... } } shape.
pluginpack.json declares only the server files that ship:
{
"files": {
"mcp/start.mjs": "start.mjs",
"mcp/dist/index.js": "dist/index.js"
}
}
The MCP source and tests remain in mcp/; only declared shipping files are
emitted. Add "mcp" to exclude to omit the complete capability for a target.
Author config.json once, in either the
Agent Plugins form (an explicit
type per server, ${PLUGIN_ROOT} / ${PLUGIN_DATA}) or the Claude-style
form (type optional, ${CLAUDE_PLUGIN_ROOT}). Pluginpack renders it into
each target's MCP dialect, so you don't need a per-target config.json
override just to change a variable name:
| Target | File | Plugin root / data variables | Transport labels |
|---|---|---|---|
claude | .mcp.json at the plugin root | ${CLAUDE_PLUGIN_ROOT} / ${CLAUDE_PLUGIN_DATA} | streamable-http becomes http |
cursor | .mcp.json, referenced from plugin.json | ${CURSOR_PLUGIN_ROOT} / none (a build error) | type dropped for stdio and HTTP (Cursor infers them) |
copilot | .mcp.json, referenced from the marketplace | ${PLUGIN_ROOT} / as authored | streamable-http becomes http |
codex | mcp.json (Agent Plugins, with $schema) | ${PLUGIN_ROOT} / ${PLUGIN_DATA} | explicit type: stdio, streamable-http, sse |
agent-plugins | mcp.json (Agent Plugins, with $schema) | ${PLUGIN_ROOT} / ${PLUGIN_DATA} | explicit type: stdio, streamable-http, sse |
antigravity | mcp_config.json beside plugin.json | as authored | as authored |
For Agent Plugins output, a ${<root variable>}/bin/server command becomes
./bin/server. Anything a conforming client would silently skip fails the
build instead:
command, including a ./ or ${<root variable>}/
path followed by arguments (./bin/server --stdio): clients run the whole
value as one file name, so arguments go in args;${VAR} other than ${PLUGIN_ROOT}/${PLUGIN_DATA} (left unexpanded),
or any placeholder in url or headers;cwd that isn't ./<path>, ${PLUGIN_ROOT}, ${PLUGIN_DATA}, or a
path under one of them that stays inside it (. and bare relative paths
fail; omit cwd to use the plugin root);type, command, args, env, cwd (stdio) or type,
url, headers (remote). The one exception is the codex target, which
also passes through a per-server extensions["com.openai"] object, where
Codex reads MCP auth. The agent-plugins target rejects it, even with
clients.codex, because other clients skip a server that has it. To build
both targets, set overrides: "overrides/codex/<plugin>" on the codex
plugin and put a copy of config.json with the extension in
overrides/codex/<plugin>/mcp/config.json. See
CONFORMANCE.md;env entry named PLUGIN_ROOT or PLUGIN_DATA, or a header set twice;http to a non-loopback host.With format: "legacy", codex writes .mcp.json as authored.
On every target, a ./bin/server or ${<root variable>}/bin/server command
must name a file the plugin ships, and so must an args entry that starts
with ${<root variable>}/, such as ${PLUGIN_ROOT}/dist/server.js (a
directory the plugin ships also counts). If the build output has no such
file, the build prints a warning, because you can still add the file after
the build. validate fails an Agent Plugins package whose command file is
missing.
The claude target rejects a stdio server that sets cwd. Claude Code
ignores cwd for plugin MCP servers and starts them in the user's working
directory; it resolves ${CLAUDE_PLUGIN_ROOT} only in command, args, and
env (plugins reference,
MCP docs). Remove cwd and write
plugin paths in args with the root variable, for example
"args": ["${PLUGIN_ROOT}/server/index.js"]. If other targets need the
cwd, keep it in the shared config.json, set
overrides: "overrides/claude/<plugin>" on the claude plugin, and put a copy
without cwd in overrides/claude/<plugin>/mcp/config.json.
A ./bin/server command or ./server cwd becomes
${<root variable>}/bin/server for targets whose clients don't resolve
plugin-relative paths. Config already written in
a target's own dialect is emitted unchanged, except that Cursor output drops
type from stdio and HTTP servers, because Cursor infers the transport.
Legacy 0.10 sources may still declare additionalFiles in
plugin.pluginpack.json. New MCPs use mcp/pluginpack.json instead.
{
"additionalFiles": {
"dist/index.js": "dist/index.js",
"start.mjs": "start.mjs",
"package.json": "package.json"
}
}
The map is destination (emitted plugin root relative) -> source (source plugin relative). Files are emitted at the plugin root after partial substitution for supported text types (see Partials), are tracked as managed output, and support target overrides: a targets/<host>/<source> file wins for that host. A destination that collides with a component or static file is an error, and pluginpack does not build bundles — produce build output before running pluginpack build so the declared source paths exist.
Skills, agents, commands, and rules often need to repeat the same procedural prose — a consistent auth flow ("try OAuth first, fall back to a token") is a good example. Rather than copy-pasting it into every file, author it once under a partials/ directory and reference it with a {{> name}} tag:
partials/auth.md
shared/acme/skills/release-notes/SKILL.md -> contains {{> auth}}
shared/acme/skills/changelog/SKILL.md -> contains {{> auth}}
Point source.partials at that directory in pluginpack.config.ts:
export default defineConfig({
source: { partials: "partials" },
// ...
});
Partials are project-level (shared across every source plugin, not scoped to one), and may reference other partials — nested composition resolves in one pass, though a circular reference (A includes B includes A) is a build-time error. A tag alone on its own line — the common case — leaves no blank line behind.
Substitution runs on every .md/.mdc/.markdown/.txt file pluginpack emits, whatever the case of the extension (NOTES.MD counts) — skills, agents, commands, rules, declared shipping files, and a target's repositoryFiles — via the real mustache library.
A tag that cannot be resolved fails the build. A {{> name}} reference naming a partial that does not exist is an error listing the available partials and the nearest match, rather than rendering as nothing — silently dropping a section out of a shipped skill file is worse than a red build. The same applies to a malformed reference ({{> }}), to a tag inside a partial's own body, and to a tag that substitution itself would assemble from neighboring text, such as a partial that ends in { placed before {> name}}.
Substitution keeps the bytes of the file intact:
Only partial tags are substituted. Any other {{...}}-shaped text — documentation about Handlebars, Jinja, Go templates, or Mustache itself, or a curly-brace code sample — is emitted exactly as authored, including text Mustache could not parse as a template at all. To write a literal partial tag, escape it with a backslash:
Reference a partial by writing \{{> auth}} in a skill file.
That emits {{> auth}} verbatim. A backslash directly before a tag always escapes it, so \\{{> auth}} emits \{{> auth}}. To emit a literal backslash followed by the partial's content, put the backslash in a partial of its own (for example partials/backslash.md, holding only \ with no trailing newline) and write {{> backslash}}{{> auth}}.
Because substitution does not run on other file types, a tag authored in (say) a .yaml reference file or a .py script would otherwise ship through untouched; build fails on any such tag left in emitted output, and validate reports it in an already-generated target repo. validate does not report tags in .md/.mdc/.markdown/.txt files: there, the only tag a build leaves is an escaped one.
For a plugin emitted at the output root (path: "."), validate does not walk the whole root, because authored sources and dependencies live there too. It checks the files listed in the target's managed manifest (.pluginpack/<target>.json) and the files the app reads from the plugin root, such as the plugin manifest and MCP config. Without a managed manifest, it checks the recognized component directories (skills/, scripts/, and so on) instead.
There are two reasonable alternatives when the single-repo shape is not enough:
For one target, copying files by hand may be enough. pluginpack starts to earn its keep when you need deterministic manifests, target-specific overrides, validation, and CI checks across multiple target repos.
pluginpack diff is designed for automation. It builds into a temporary directory, compares generated managed files against an existing plugin repo, and exits non-zero when the plugin repo is stale:
pluginpack diff --target cursor --against ../cursor-plugins
pluginpack diff --target claude --against ../claude-plugins
Use that in CI to fail clearly or to trigger an action that opens a PR against the generated plugin repo.
When a generated target repo intentionally owns a path, add ignoredDiffPaths to that target config. Entries are target-output-relative paths; a directory entry ignores everything below it.
To publish generated-repository files, point repositoryFiles at a directory.
Every file below it is copied to the target output root and managed like the
rest of the artifact.
pluginpack.config.ts exports a config object (wrap it in defineConfig for types). src/schema.ts is the source of truth; this table enumerates every field. Paths marked safe relative reject absolute paths and .. escapes.
Keys stripped by config parsing produce a warning that names the config file and the dotted field path, once per file and key per process. Unknown keys in plugin.pluginpack.json (both canonical and legacy sources) and mcp/pluginpack.json produce manifest warnings. Fix the spelling or remove the key. The manifest and entry passthrough objects preserve arbitrary keys without warnings. Unknown package client names still fail validation.
Top level
| Field | Type | Required | Meaning |
|---|---|---|---|
name | string | yes | Marketplace/source name written into generated manifests. |
version | string | yes | Default version stamped into manifests (per-target/plugin overridable). |
source | object | legacy | 0.10 discovery/partials config; direct plugin sources now live on emitted plugins. |
metadata | object | no | Shared metadata merged into manifests (see metadata). |
targets | object | yes | Per-target output config, keyed by target name (see targets.<name>). |
Legacy source (0.10 migration compatibility)
| Field | Type | Required | Meaning |
|---|---|---|---|
plugins | string | no | Directory to discover source plugins from. Defaults to plugins. |
skills | string | no | Repo-level skills directory; creates a root source plugin from sibling component dirs. |
partials | string | no | Directory of reusable {{> name}} text fragments, shared across every source plugin (see Partials). Safe relative: it must sit inside the project. |
rootPlugin | object | no | Metadata for that root skills plugin. Accepts all metadata fields plus id, name, description. id is the source-plugin name used in each target's from array. |
metadata (and source.rootPlugin)
| Field | Type | Meaning |
|---|---|---|
displayName | string | Human-readable name. |
description | string | Short description. |
author | { name, email?, url? } | Author identity (name required). |
owner | { name, email?, url? } | Marketplace owner (name required). |
homepage | string | Homepage URL. |
repository | string | Repository URL. |
license | string | SPDX license id. |
logo | string | Logo path or URL. |
keywords | string[] | Marketplace keywords. |
category | string | Marketplace category. |
tags | string[] | Free-form tags. |
targets.<name> — <name> is one of cursor, claude, antigravity, copilot, codex, agent-plugins.
| Field | Type | Required | Meaning |
|---|---|---|---|
outDir | string | yes | Output directory for this target, relative to the config root. May be outside the project; it may not sit inside a source path. |
format | "agent-plugins" | "legacy" | no | codex only. "legacy" keeps the .codex-plugin/plugin.json layout; deprecated in 0.12, removed in 0.13. Default "agent-plugins". |
plugins | record | yes | Emitted plugins, keyed by emitted plugin name (see targets.<name>.plugins.<name>). |
marketplaceDir | string (safe relative) | no | Compatibility field for cursor / claude: only the fixed host directory (.cursor-plugin / .claude-plugin) is accepted. Other targets do not use it. |
pluginRoot | string (safe relative) | no | Override the plugin root dir (claude; defaults to plugins). |
version | string | no | Override the version for this target (defaults to top-level version). |
manifest | object | no | Deep-merged into the generated marketplace manifest. An error on agent-plugins; use clients.<client>.manifest. |
clients | { codex?, copilot? } | no | agent-plugins only. Clients each package serves; each gets a marketplace. clients.<client>.manifest is deep-merged into that client's marketplace. |
ignoredDiffPaths | string[] | no | Output-relative paths diff ignores (a dir entry ignores everything below it). |
repositoryFiles | string (safe relative) | no | Directory copied recursively into the generated repository root. |
rootFiles | record (safe relative) | no | Legacy 0.10 output path → source path map; migrate to repositoryFiles. |
updateCheck | { repository? } | no | Generate a session-start update-check hook (claude/cursor only; see Update Check). |
repository | string | no | Repo this target's output lives in, for install-info (defaults to metadata.repository). |
Claude Code and Cursor require their native manifest paths: .claude-plugin/
and .cursor-plugin/, respectively. Remove a custom marketplaceDir; use
outDir to move the generated repository instead. Claude's validator reports
No manifest found in directory. Expected .claude-plugin/marketplace.json or .claude-plugin/plugin.json
for a relocated manifest. Cursor's reference
says to place the marketplace at .cursor-plugin/marketplace.json.
targets.<name>.plugins.<name>
| Field | Type | Required | Meaning |
|---|---|---|---|
source | string (safe relative) | yes | Direct path to the plugin's shared authored source. |
overrides | string (safe relative) | no | Target-specific directory applied after source; may add or replace files. |
include | string[] | no | Exact content kinds to include (skills, agents, rules, assets, static, mcp, etc.). |
exclude | string[] | no | Content kinds removed from the target defaults. |
from | string[] (min 1) | legacy | 0.10 source-plugin composition; cannot be combined with source. |
path | string (safe relative) | no | Output path for the plugin, relative to outDir. Defaults to the plugin name (or pluginRoot/<name> for claude). |
version | string | no | Per-plugin version override. |
displayName | string | no | Per-plugin display name. |
description | string | no | Per-plugin description override. |
manifest | object | no | Deep-merged into the generated plugin manifest. |
entry | object | no | Deep-merged into the generated marketplace entry (the object in the marketplace plugins array). Use for target-specific entry fields pluginpack can't derive — e.g. Codex policy/category. An error on agent-plugins; use clients.<client>.entry. |
clients | { codex?, copilot? } | no | agent-plugins only. clients.<client>.entry is deep-merged into this plugin's entry in that client's marketplace (e.g. clients.codex.entry for Codex policy/category). Each key must be listed in the target's clients. |
components | string[] | legacy | 0.10 include-only name; migrate to include. |
updateCheck | false | no | Opt this plugin out of the target's update-check hook. |
Everything the CLI does is exported from the package entry, so you can script builds (the pluginpack-action consumes these directly):
import {
defineConfig,
loadConfig,
build,
diffTarget,
validateOutput,
prune,
clean,
buildInstallSnippet,
getInstallSnippetCitation,
getSupportedInstallTargets,
getUnsupportedInstallTargets,
} from "@gleanwork/pluginpack";
| Function | Returns | Purpose |
|---|---|---|
defineConfig(config) | PluginpackConfig | Identity helper that types pluginpack.config.ts. |
loadConfig(cwd?, configPath?) | Promise<ResolvedProject> | Resolve config and discover source plugins. |
build(options?) | Promise<Artifact[]> | Emit configured targets; writes to disk unless options.dryRun. |
diffTarget(options) | Promise<DiffResult> | Build into a temp dir and compare against an existing repo. |
validateOutput(target, dir) | Promise<ValidationResult> | Validate an existing target output directory. |
prune(options?) | Promise<CleanupResult[]> | Remove stale managed files no longer emitted by the config. |
clean(options?) | Promise<CleanupResult[]> | Remove all managed files for configured targets. |
buildInstallSnippet(target, params) | InstallSnippet | The install command/URL for one target (see Install Snippet). |
getInstallSnippetCitation(target) | Citation | The documentation source backing that target's install snippet. |
getSupportedInstallTargets() | TargetName[] | Targets with a real install snippet today. |
getUnsupportedInstallTargets() | TargetName[] | Targets with none (today, agent-plugins). |
Option objects:
build — { cwd?, configPath?, target?, outDir?, dryRun? }diffTarget — { cwd?, configPath?, target, against }prune / clean — { cwd?, configPath?, target?, dryRun?, force? }The result and config types (Artifact, DiffResult/DiffEntry, ValidationResult/ValidationIssue, CleanupResult/CleanupEntry, ResolvedProject, PluginpackConfig, TargetConfig, TargetName, Citation, InstallSnippet, …) are all exported for use in TypeScript.
PluginpackConfig describes authoring input. defineConfig is an identity helper; it does not parse or apply defaults. loadConfig returns ResolvedProject.config as ResolvedPluginpackConfig, the validated schema output. The declaration build derives structural types from those same schemas, so consumers do not need Zod types or ambient Node types. Artifact.files values are string | Uint8Array, including Node buffers. Decode byte values with Buffer.from(value).toString("utf8") or new TextDecoder().decode(value).
A few things worth knowing about this surface before depending on it:
ResolvedProject.sourceRoot, plugins, partials, and source expose internal build state. They are not supported extension points, and their shapes may change.TargetName is "claude" | "cursor" | "antigravity" | "copilot" | "codex" | "agent-plugins" today, with no public API for registering another target — adding one means a PR to this repo (see PluginTargetDefinition in src/targets/types.ts, not exported). There is no supported third-party target-extension mechanism.package.json's exports map has no require condition; a CommonJS consumer needs dynamic import(). This is a deliberate choice, not a tsup default left unexamined.Artifact.files and ResolvedProject.plugins are Maps, not plain objects. JSON.stringify() on either silently produces {} — iterate with for...of/Object.fromEntries() instead of serializing directly if you need to log or transport a result.initCreate a starter pluginpack.config.ts for every target and a shared/example source plugin. Refuses to overwrite existing files.
pluginpack init [options]
Examples:
pluginpack initExit codes:
buildCompile configured source plugins into target-native plugin payloads.
pluginpack build [--target copilot|antigravity|cursor|claude|codex|agent-plugins] [--out-dir <path>] [--dry-run]
Options:
--target <target>: Build only one configured target.--out-dir <path>: Override the configured output directory for the selected target.--dry-run: Resolve and print planned managed output paths without writing files.Examples:
pluginpack buildpluginpack build --target cursorpluginpack build --target claude --dry-runExit codes:
validateValidate an existing target output directory for native manifest, path, and frontmatter requirements.
pluginpack validate --target copilot|antigravity|cursor|claude|codex|agent-plugins [--dir <path>]
Options:
--target <target>: Required target validator.--dir <path>: Directory to validate. Defaults to the configured target outDir.Examples:
pluginpack validate --target cursor --dir ../cursor-pluginsExit codes:
diffBuild into a temporary directory and compare generated managed files with an existing target repo.
pluginpack diff --target copilot|antigravity|cursor|claude|codex|agent-plugins --against <path>
Options:
--target <target>: Required target to build and compare.--against <path>: Existing target repo or output directory to compare against.Examples:
pluginpack diff --target cursor --against ../cursor-pluginsExit codes:
pruneRemove stale managed files that are no longer emitted by the current config.
pluginpack prune [--target copilot|antigravity|cursor|claude|codex|agent-plugins] [--dry-run]
Options:
--target <target>: Prune only one configured target.--dry-run: Print stale managed files without deleting them.--force: Delete even paths that resolve inside the source tree or config.Examples:
pluginpack prunepluginpack prune --target claude --dry-runExit codes:
cleanRemove all managed files in configured target outputs, including those of a target the config no longer builds there.
pluginpack clean [--target copilot|antigravity|cursor|claude|codex|agent-plugins] [--dry-run]
Options:
--target <target>: Clean only one target. It may be one the config no longer builds, if its manifest is in a configured outDir.--dry-run: Print managed files without deleting them.--force: Delete even paths that resolve inside the source tree or config.Examples:
pluginpack cleanpluginpack clean --target cursor --dry-runExit codes:
install-infoPrint the real install command or URL for a target's built marketplace.
pluginpack install-info [--target copilot|antigravity|cursor|claude|codex|agent-plugins] [--json]
Options:
--target <target>: Print only one configured target's install info.--json: Print machine-readable JSON instead of text.Examples:
pluginpack install-infopluginpack install-info --target claudepluginpack install-info --jsonExit codes:
docsGenerate the README CLI reference section from command metadata.
pluginpack docs [options]
Options:
--check: Fail if README.md is not up to date.Examples:
pluginpack docspluginpack docs --checkExit codes:
FAQs
Compile one source of agent plugins — skills, commands, hooks, and MCP servers — into the native plugin format each AI app expects.
The npm package @gleanwork/pluginpack receives a total of 2 weekly downloads. As such, @gleanwork/pluginpack popularity was classified as not popular.
We found that @gleanwork/pluginpack demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 10 open source maintainers collaborating on the project.

Security News
Cloudflare introduces tsrs, a Rust port of TypeScript’s compiler promising faster checks and lower memory use, with the TypeScript team’s help.

Security News
The PHP Foundation says Chainguard broke a security embargo ahead of critical RCE patches, as leaks disrupted the coordinated release.

Security News
PHP releases emergency patches for two critical RCE vulnerabilities as agencies urge organizations to take unpatched sites offline.