Host installation
The composite plugin root build emits contains one generated INSTALL.md, a section per
selected host, with commands that use the bundle's real plugin and marketplace names. Every
command in it is a host command (claude plugin …, codex plugin …) or the bundled
install.mjs, so a consumer who receives the built root, from npm, a marketplace checkout, or
a zip, never needs the agent-bundle CLI to install, reinstall, or uninstall it; INSTALL.md
says so and marks the CLI optional wherever it is mentioned. The framework CLI performs those
same operations for developers who have it, and every host installs from the same directory:
--from names the composite root: the directory that holds agent-bundle.manifest.json. The
installer reads that one file and nothing else to learn what it is installing, the plugin name
and version come from application, the marketplace name from projections[host].marketplace,
and the file the host is pointed at from projections[host].documents.plugin (or
documents.entry for Amp; see the
Artifact manifest). It needs no project sources, and there
is no <from>/<host> lookup: builds write no artifact/<host> subdirectory, and the root is the
bundle. AB7001 refuses a root whose manifest is missing or not canonical, that has no projection
for the host, whose projection has no host plugin manifest (or whose pointed file is absent), that
declares no marketplace identity for claude or codex, or whose plugin name is not a safe local
plugin name for cursor.
What each host accepts
Because the Claude and Codex projections always ship local marketplace manifests, their public
CLIs can install the root directly. The installer fails with a typed diagnostic when the
selected host binary is unavailable, rather than reporting a success it did not achieve. Host
installation diagnostics are the AB700x family: bundle identity, host availability, scope,
command failure, and collision checks.
Amp is experimental: contract-verified, runtime-unverified. It does not expose a non-interactive
reload or standalone validator. Installation therefore returns the interactive reload step and
never runs it. In this account-less validation environment, amp plugins list and
amp skills list --json require login; live registration, enabled state, and tool execution are
recorded as unverified, not unsupported.
The standalone installer
A root built with cursor or portable among its targets includes an install.mjs that copies
the root into ~/.cursor/plugins/local/<name> without overwriting collisions:
Its staged copy is idempotent for identical content and refuses version or content collisions. It
never invokes sudo and never changes PATH. When agent-bundle.manifest.json is present, the
installer reads the selected projection's plugin and MCP pointers from it; conventional
.cursor-plugin/* and root Agent Plugins paths are fallback discovery for manifest-less packs only.
For a root Cursor loads as an Agent Plugins 1.0.0 pack, portable selected without cursor,
so the root plugin.json is the only manifest Cursor finds, the installer also expands the
standard's placeholders in the Cursor copy of mcp.json, because Cursor 3.18.25 does not: it leaves
${PLUGIN_ROOT} / ${PLUGIN_DATA} unexpanded in args, env values, and cwd, provides no
PLUGIN_ROOT / PLUGIN_DATA variables (spec §9.1), defaults an omitted cwd to the home
directory, and resolves plugin-relative ./ commands against the workspace folder (spec §7.2.1),
so every spec-shaped stdio server fails to spawn. The Cursor copy gets the absolute plugin root,
~/.cursor/agent-bundle/plugin-data/<name> (created by the installer) as the data directory, a
plugin-root cwd, resolved ./ commands, and PLUGIN_ROOT / PLUGIN_DATA in each stdio
server's environment. The bundle itself is untouched and stays spec-conformant for other Agent
Plugins clients; the pre-expansion document is kept in the install receipt (cursorExpansion),
and agent-bundle doctor --host cursor proves the expansion with AB7326. Nothing here is
Cursor behaviour: the provenance is derived, and a root that carries .cursor-plugin/plugin.json,
cursor selected, is copied unchanged, because Cursor loads that manifest instead.
Artifact validation rejects a root whose required install surface is missing, so a bundle cannot ship without the installer it promises.
The npm-installed CLI
The npm package root contains every manifest-owned composite-plugin byte plus npm-only metadata
and authored package entries. Commands such as validate --artifact still take the bare composite
artifact; install --from is manifest-aware and accepts either root. For a generated routed CLI,
package.json points its bin at the same bin/<name>.mjs executable the artifact exposes,
without a second compilation or wrapper. Its complete command surface, including web, therefore
works from node_modules exactly as it does from a copied artifact. No npm lifecycle performs a
host installation, installing a package never mutates a host's plugin state.
Repository AGENTS.md instructions are copied as authored package metadata. They are never
replaced by generated projection instructions; a path collision fails the package build.
Install any supported host explicitly from that root with
npx agent-bundle install <host> --from node_modules/<package>. This manifest-aware path excludes
npm-only metadata and authored package entries from host ownership comparisons. The packed host
proof reconstructs the manifest-owned artifact from the installed package and exercises
agent-bundle install --from after deleting the source checkout.
A package-bound installer bin
A published plugin that wants <name> install <host> without asking its users to install the
agent-bundle CLI declares one bin entry and
delegates to agent-bundle/install:
runInstallCli is the agent-bundle CLI's own install <host>, uninstall <host>, and
doctor commands, the same flags, receipts, replacement rules, --plan, data policy, --json
output, and exit codes, with the bundle root pinned to from, so the bin exposes no --from and never searches for another artifact
root. The package build inlines the lifecycle into the executable; the installed package needs no
agent-bundle at run time. The entry also exports installBundle, uninstallBundle, runDoctor,
and the format* helpers for a bin that composes its own command surface. The read-only status
check is doctor --host <host>: it reports the installed copy as current, stale,
version-mismatched, foreign, or absent.
Exit codes follow the agent-bundle CLI. 0 is success. doctor always writes its report (human
or --json) to stdout and exits 1 when that report carries an error diagnostic, read the report,
not stderr. An install or uninstall that fails, or any command that throws, writes its
diagnostics as one JSON line to stderr and exits 1. A usage error exits 2.
Development installs are a different thing
agent-bundle dev --install-host <host> maintains a marked development install that follows
successful rebuild epochs, with atomic generation switching and a stable proxy command. That is
covered in Developer Workbench and is not the same operation as
agent-bundle install. Amp is intentionally not a development-install host; use its owned
agent-bundle install amp copy and the interactive reload action.
Reinstalling after a rebuild
Every emitted installer, agent-bundle install <host> and the standalone install.mjs,
shares one replace policy. An identical copy is an already-installed
no-op. A copy of the same version whose content hash differs is replaced automatically, so
rebuilding without a version bump no longer needs an uninstall and rm -rf. A different version is
refused with AB7005 unless you pass --replace, and a foreign directory, one
this plugin's installer did not place, is refused either way. A copy placed before install
receipts existed carries no receipt, so it is foreign too, byte-identical or not: remove it by hand
and reinstall. The standalone install.mjs hashes a receipt-less destination from the files
present there, so the refusal can name the content comparison. When agent-bundle.manifest.json is present, the artifact hash uses that
manifest's files[], and a listed path missing from the artifact fails the run. A listed path
missing from a foreign or staged destination is omitted from its hash, so the hashes differ and
install refuses that copy instead of throwing ENOENT. Owned same-version copies can be repaired.
Cursor and Amp copies carry an install receipt
(.agent-bundle-install.json: plugin, version, host, content hash, owned files); replacement is in
place and touches owned files only, never unowned entries such as a state/ directory beside the
plugin. Artifact builds keep framework state under
~/.agent-bundle/state/<plugin>-<digest> instead (AGENT_BUNDLE_STATE_ROOT overrides that
location); uninstall --purge-data --confirm-purge removes only roots whose receipt proves that
installation owns them. Pre-existing, shared, marker-less, and otherwise unproven override roots
are retained. Claude replacement runs
claude plugin uninstall --keep-data before reinstalling because plugin update is
version-gated; Codex replacement is add-only (codex plugin add) so plugin settings and nested
MCP overrides in config.toml survive. Native add would reset a plugin-level enabled = false
to true; the native plugin CLI has no qualified settings-preserving update API, so the installer refuses
disabled or unknown-enablement replacements (AB7004) and leaves those installs unchanged.
The emitted INSTALL.md
documents the same recipe per host.
The framework CLI derives the artifact-side hash and copy set from
agent-bundle.manifest.json files[], verifies every listed digest, and does not walk the artifact
directory. The manifest itself and a conventional operator .env / .env.local overlay are also
copied and hashed; an unlisted stray file is ignored. A missing or changed listed file fails as
AB7001.
Operator configuration: the installed pack's .env
A plugin configured through environment variables, session cookies, API tokens, a host name, is
configured at the install, not in the artifact: manifest env is compiled, content-addressed, and
re-emitted on every build, which is the wrong place for a secret. Instead, put a .env file at the
plugin root of the installed copy (the directory that holds .claude-plugin/, .codex-plugin/,
or .cursor-plugin/, beside mcp/, hooks/, and bin/), optionally with a .env.local
beside it:
Every shell the pack emits reads that file when it starts, the
stdio MCP server entries, the hook wrappers that
run plugin code, and the artifact CLI bin/<name>.mjs, under one rule:
The same order agent-bundle mcp run composes, manifest env under the files under the
operator's exports. A host hands a stdio server its manifest env and its own exports as one
environment, so the generated entry carries the server's declared env block as build-time
literals and treats a variable that still holds its manifest default as unset: the file may
override it. A variable whose value differs from the default was exported by the host or the
operator and is kept. Two consequences follow. An operator export that happens to equal the
manifest default is indistinguishable from the pass-through and yields to the file too. A manifest
value carrying a path token (AGENT_BUNDLE_PLUGIN_ROOT, or a declared env value that uses one)
is expanded by the host before launch, so it never equals its literal default and is always kept.
Hook wrappers and the artifact CLI have no manifest env, so for them the host environment is
simply what the host exported.
The location is the plugin root the pack resolved, AGENT_BUNDLE_PLUGIN_ROOT when the host set
it, otherwise the directory above the shell's own (mcp/, hooks/, bin/). AGENT_BUNDLE_ENV_FILE
names one or more files (platform path delimiter) to read instead, and AGENT_BUNDLE_ENV_FILE=none
disables the layer. The grammar is dotenv's: KEY=value, export KEY=value, # comments, single-,
double-, or backtick-quoted values (double quotes expand \n), multi-line quoted values, and no
${VAR} interpolation. A missing file is the normal case and costs nothing; an unreadable one is
skipped so the pack still starts. Values are never logged. The file is not owned by the install
receipt, so install --replace and a same-version rebuild leave it in place, and uninstall never
removes unowned entries: the file survives an uninstall too (Doctor then reports the directory as a
remnant, AB7307), so delete it by hand when the credentials should go. The layer is the first
import of every shell that applies it, ahead of the server module, the hook handler, and the CLI
route, provider, and state modules, so plugin code sees the composed environment even in a
process.env read at module top level, not only when it runs. agent-bundle mcp run
composes the same layers itself and hands --env-file / --no-env down as
AGENT_BUNDLE_ENV_FILE, so a rehearsal and an installed pack read the same files.
agent-bundle doctor reports whether an installed copy carries .env or .env.local and how many
variables each declares (AB7331, informational), never a name or a value.
Uninstalling by receipt
Every install writes a lifecycle receipt (agent-bundle-install-receipt/2): version, content
hash, delivery mode, scope, the owned files and directories, the host directories the installer
created, the host registrations it performed, and timestamps. Cursor local copies carry it as
.agent-bundle-install.json; Amp project/system copies carry the same sidecar inside their generated
directory; Claude, Codex, and Cursor marketplace-mode installs keep theirs under
<host root>/agent-bundle/receipts/. uninstall removes exactly what the receipt owns and
reverses exactly the registrations it recorded, never anything else; unowned entries are listed
as retained, a state/ directory beside the plugin included. The framework state roots the
receipt records with ownership evidence, derived web-data, and for a Cursor copy of an Agent Plugins
pack the PLUGIN_DATA directory the receipt records are kept unless
you pass --purge-data --confirm-purge, and the result states honestly what the host itself decided where
Agent Bundle cannot (Claude orphans its cached copy for a ~14-day grace period; Codex deletes the
cached tree and offers no keep-data option). A missing store receipt for a host-registered install
(AB7009) or a content mismatch (AB7007) is refused unless --force. Even with --force, a
copy without a store receipt is never purged: --purge-data --confirm-purge is refused (AB7009) because nothing
proves the bundle owns its data, so uninstall without it and remove the data by hand. A Cursor directory without
a receipt, or one that belongs to another plugin, is foreign and refused regardless (AB7007);
a second run is a not-installed no-op. Only format 2 receipts are read; a copy carrying an older
receipt is foreign. When a receipt records no state ownership, a root resolved from the current
environment or home is listed as unproven and retained; --keep-data cannot turn that observation
into later purge authority.
The host-install proofs snapshot an isolated home before install and after uninstall: byte-identical for Cursor local and the portable Agent Plugins package, and zero Agent Bundle residue plus an enumerated set of host-owned bookkeeping for Claude and Codex.
Inspect an install without changing it
Doctor is read-only. --from is the same composite root install takes, read the same way: the
plugin identity and each inspected host's plugin manifest come from agent-bundle.manifest.json
(application, projections[host]), and a root the manifest does not describe for that host is
AB7001. It probes hosts, inventories installed bundles, compares them against a
supplied bundle, checks registration proof, samples runtime endpoint health and identity,
inventories durable state and the operator .env layer (AB7331), and re-runs the pinned process-free document and loader validators over
installed bytes. It never repairs anything. With --from, it reports the installed copy per host as
current, stale (AB7308), version-mismatch (AB7309), foreign (AB7321), load-failed
(AB7325), or not-installed (AB7307), and its lifecycle stage, placed → registered → enabled → active, with
every stage either observed from the host or typed unavailable with the reason (AB7330). load-failed means
Claude Code lists the copy but refused to load it, its claude plugin list --json row carries an errors array,
which Doctor reports verbatim in place of current, because a refused copy contributes no hooks, MCP servers, or
skills to a session. The installer reads the same array: agent-bundle install claude fails with AB7006 when the
freshly installed (or byte-identical existing) copy carries errors, instead of reporting installed. Doctor also
inventories the receipt store under each host root and cross-checks it against the host (AB7328).
For Claude, Doctor also reads each row's enabled flag and runs the Claude Code developer
validator. A copy listed with enabled: false (switched off with claude plugin disable or the
/plugin menu) is reported as disabled in the inventory, keeps its content verdict in the
--from comparison, and adds an AB7327 warning with the claude plugin enable command, a
disabled plugin loads nothing, and reinstalling does not enable it. The validator
(claude plugin validate --strict against plugin.json, then marketplace.json, the same runs
build and validate --artifact perform) covers the --from bundle and every installed copy, and
its findings keep their AB6019–AB6022 codes prefixed with Bundle at … or
Installed copy at … (scope …). Doctor never runs the --plugin-dir load check twice: its
registration proof and the rows' errors already hold that verdict.
Next
- Preview packages: where installable tarballs come from today.
- Artifact validation: the checks that gate a bundle before it is installed.