For AI agents: the complete documentation index is available at https://scriptedalchemy.github.io/agent-bundle/llms.txt, the full documentation bundle is available at https://scriptedalchemy.github.io/agent-bundle/llms-full.txt, and this page is available as Markdown at https://scriptedalchemy.github.io/agent-bundle/guide/distribution/validation.md.
Skip to main content
  • English
  • Artifact validation

    Validation answers two different questions with one command. Against source it asks whether the project describes a legal bundle; against an artifact it asks whether the emitted bytes are the bytes the manifest promised.

    npx agent-bundle validate --root .                        # project source
    npx agent-bundle validate --artifact artifact --strict    # built bytes, no sources needed

    validate --artifact, mcp, and hooks all work against a built artifact with the project sources deleted. That is deliberate: an artifact that only validates while its source tree is present is not really self-contained, and the packed-deleted-source proof level in Testing exists for the same reason.

    Content-addressed, not path-existence

    agent-bundle.manifest.json records a SHA-256 digest for every emitted file, including copied scripts and assets. Artifact validation compares real bytes against those digests, so a hand-edited generated file fails rather than passing because the path still exists. Referenced files are checked too, a manifest-declared logo that is missing from the artifact or escapes the deploy tree reports AB6025. A listed compile evidence record that does not match the manifest file table reports AB6039.

    Self-containment is proven from the compilation's externals (AB6005 at compile time). The compiler service lowers every host-pack surface and package-build entry (dist/bin/*.js, a rendered route's Flight worker .mjs, and the lib entry), plus every MCP App view. The framework-owned ArtifactDependencyAuditPlugin taps thisCompilation to record every module Rspack kept external, and the service reads that evidence before trusting an asset. Only Node built-ins (node:fs, fs), pnpapi, and an emitted sibling of the same artifact may remain external; any other request fails whatever spelling Rspack emitted (import, require, or its createRequire shim). An expression request (import(expr), require(expr)) is outside the compiler's view: Rslib's profile leaves it verbatim without parsing it, so it is neither bundled nor external.

    The emitted-module walk remains behind that compiler evidence for exactly what the compiler cannot see. It reads every emitted .js/.mjs module as an ES module and, in any of them, compiled ones included, fails an expression import() (AB6005 has a non-literal dynamic import). A module the compile evidence record proves, a manifest bundle file the record covers with the same bytes, from a build without a tools hatch, is lexed, not parsed, and each literal import it still carries is held to the record: a Node built-in or one of the file's recorded externals passes; any other request is one the build was told to ignore (rspackIgnore/webpackIgnore, Rspack leaves it verbatim with no module, external, or warning) and fails AB6005 loads "<request>", which the compiler neither bundled nor recorded as an external. A matching digest proves the bytes are the compiler's, not that every import in them was resolved. Every other module is parsed in full and its imports resolved against the manifest, a generated install.mjs, a copied consumer script, every module of an artifact without a record, and every module of a build whose hatch could have rewritten the emitted assets (the record's coverage.rewritable). Those findings are the residual AB6005 forms: unsupported or invalid specifiers, targets missing from or outside the emitted tree, invalid syntax; a dist finding names its file as dist/<path>. Prebuilt payloads (kind: 'prebuilt') stay opaque and hash-locked only, and declaration files (.d.ts) are not walked.

    During compilation, AB4837 rejects imports of compiler-carrying framework entries: agent-bundle, agent-bundle/api, agent-bundle/config, agent-bundle/eval, agent-bundle/rstest, agent-bundle/test, and agent-bundle/test/browser. The check uses transformed dependency requests and resolved package export identities, including aliases, and names the importing module. Type-only imports erased by the configured transform remain legal. Source-only inspect and validate do not establish this boundary. agent-bundle/app is a browser-safe leaf and remains allowed. An installed artifact serves an App through <plugin> web.

    Compile evidence record

    agent-bundle build writes agent-bundle.compile-evidence.json at the artifact root and lists it in agent-bundle.manifest.json as a generated file. The record is what the compiler service reported about each file it emitted, bound to those bytes. Each assets[] entry is one compiled file (bundle kind: bin/*.mjs, scripts/*.mjs, hooks/*.mjs, mcp/*.mjs, Flight workers, mcp-apps/*.html) and holds path, sha256, the kept externals (kind artifact-relative or builtin, externalType, issuers, request, userRequest, and target for a sibling), and the inlined packages. The record also carries policy closed-world-externals@1, producer { name: 'agent-bundle', rspack, version }, coverage.rewritable (true when a tools.rspack or tools.rsbuild hatch took part, so emitted bytes may differ from the module graph), and coverage.unobserved.

    agent-bundle validate --artifact re-checks a listed record against the manifest file table without reading JavaScript (AB6039). The package build returns the same record in process (PackageBuildResult.evidence, paths dist/bin/…) and writes nothing to disk. coverage.unobserved lists the load forms Rslib leaves verbatim in the bundle, so the compiler neither bundles nor records them: import(<expression>), require(<expression>), require.resolve(…), createRequire(…)(…), import.meta.resolve(…). No externals recorded therefore does not prove the absence of such a load.

    AB6039 is an error. Recovery: rebuild the artifact so its compile evidence record describes the emitted files. Message forms:

    • Compile evidence record <reason>. (the strict parser's own message, e.g. is not valid JSON)
    • Compile evidence record cannot be read.
    • Compile evidence was judged under policy <name>@<revision>; this validator applies closed-world-externals@1.
    • Compile evidence does not cover compiled file "<path>".
    • Compile evidence for "<path>" describes different bytes.
    • Compile evidence names "<path>", which the manifest does not list as a compiled file.
    • Compile evidence for "<path>" records "<request>" as a built-in; it is not one.
    • Compile evidence for "<path>" records sibling "<request>", which the artifact does not contain.
    • Compile evidence for "<path>" records "<request>" as an external; a view inlines every module it loads.

    Every diagnostic is one structured record: a stable AB code, a severity, a message, and usually a sourcePath and a recovery hint. The diagnostic-gated commands, build, prepack, validate, doctor, install, and dev, exit nonzero only when an error diagnostic is present; warnings and infos never gate a build, a validation, or a dev rebuild. eval and inspect also exit 1 for a failing or inconclusive trial or an invalid model, with no error diagnostic involved, see the CLI exit codes.

    Claude Code strict validation

    Host validation runs only for the selected projections, over the composite root. When Claude Code is on PATH and claude is among the selected projections, artifact validation runs the installed claude plugin validate over the root. agent-bundle build runs the same checks over the root it just wrote (--no-host-validation skips them; --strict promotes host warnings to build errors), and agent-bundle doctor --host claude --from <root> runs them over the root and over every installed copy Claude lists. Claude Code decides what it checks from the manifest it is pointed at, and a root that holds both .claude-plugin/plugin.json and .claude-plugin/marketplace.json is treated as a marketplace, whose validation never opens the plugin's hook, skill, agent, or command files. Agent Bundle therefore runs two commands:

    # plugin.json, hooks/hooks.json, skills/, agents/, commands/
    claude plugin validate <root>/.claude-plugin/plugin.json --strict
    # marketplace schema, plugin names, source paths
    claude plugin validate <root>/.claude-plugin/marketplace.json --strict
    • Host errors become Agent Bundle errors (AB6021).
    • Host warnings stay warnings (AB6020) unless agent-bundle validate --strict is set.
    • Each finding names the file Claude Code checked, for example (hooks hooks/hooks.json): hooks: hooks.postToolUse: unknown hook event. Agent Bundle asks for the CLI's --json report first (Claude Code 2.1.259 or later; the pinned 2.1.260 prints it) and parses the text report only when the CLI rejects the flag or the probed version predates 2.1.259. The marketplace run's plugins[0] plugin.json → copies of manifest findings the plugin run already reported are dropped.
    • A missing binary is reported as an explicit informational skip (AB6019), never as fabricated success; a probe or run that cannot complete is AB6022. A build without claude on PATH spawns once and reports the skip once.

    The load check

    Validation is not a load verdict. Claude Code 2.1.250 through 2.1.260 accept, under --strict, manifests and component files that a session then refuses to load, an invalid monitors/monitors.json, or a hooks field naming the auto-loaded hooks/hooks.json. The only place the refusal shows is the plugin's row in claude --plugin-dir <root> plugin list --json ("Plugins reference" → "plugin list"): a refused row carries an errors array with the host's message, a healthy row has no errors key. So build and validate --artifact follow the two validation runs with exactly that listing and read the bundle's <name>@inline row:

    load.statusMeaningCode
    loadedThe row has no errors.none
    refusedThe row carries errors; they are quoted verbatim.AB7325 (error; a warning when every entry is Dependency "…" is not installed, since that describes the validating machine rather than the artifact, --strict promotes it)
    unregisteredNo row for the bundle: Claude Code did not register the directory.AB7311
    failedThe listing could not be read (nonzero exit, timeout, not a JSON array).AB6022

    The listing is read-only and needs no login. Doctor does not repeat it: its --plugin-dir registration proof and the inventory rows' errors already carry the same verdicts.

    CI should use strict validation:

    npx agent-bundle validate --artifact artifact --strict

    Use --no-host-validation when a deterministic schema-only check is required, a machine without the hosts installed must still be able to run the same gate and get the same schema verdict.

    During development, load the built root for one session without installing it, then iterate:

    claude --plugin-dir artifact

    Run /reload-plugins inside that session after agent-bundle build rewrites hooks/, .mcp.json, or agents/; SKILL.md edits take effect without it. A --plugin-dir plugin is not visible to a bare claude plugin list; put the flag before the subcommand to inspect it:

    claude --plugin-dir artifact plugin list --json

    That listing is the load verdict described above, and build and validate --artifact run it for you. agent-bundle doctor --host claude --from <root> reads the same errors array on both the --plugin-dir registration proof and the installed copy and reports it as AB7325 (load-failed), and agent-bundle install claude fails with AB7006 when the installed copy carries it. The pinned Claude plugin schema also rejects hooks: "./hooks/hooks.json" up front (AB6012), since the manifest field is for additional hook files only.

    Doctor also reads each row's enabled flag. A copy that claude plugin disable switched off ("Plugins reference" → "plugin disable") is still installed and may still be byte-identical to the bundle, but none of it reaches a session; Doctor lists it as disabled, keeps its content verdict, and adds an AB7327 warning whose recovery is the exact claude plugin enable <name>@<marketplace> [--scope <scope>] command, reinstalling does not enable a disabled plugin.

    Codex and Cursor: pinned schemas

    Neither Codex nor Cursor publishes a plugin-validation developer verb today, so Agent Bundle validates their emitted JSON documents against its own vendored pinned schemas and treats host schema generators as a drift signal, never as a substitute plugin contract.

    CodeSeverityMeaning
    AB6026infoEvery Cursor report states that Cursor publishes no plugin-validate verb, and names the vendored schema pin used locally.
    AB6027errorA required generated Cursor document is missing, or a present plugin, marketplace, MCP, or hooks document is unreadable, invalid JSON, or rejected by its pinned schema. The hooks document is the one .cursor-plugin/plugin.json hooks names (.cursor-plugin/hooks.json, beside the manifest, or an inline object), falling back to hooks/hooks.json only when the field is absent; a named file that is missing or leaves the plugin root is an error.
    AB6028errorGenerated bytes violate pinned Cursor loader evidence: manifest-candidate precedence selects a fallback manifest, a symlink resolves outside the bundle, or CURSOR_PLUGIN_ROOT appears outside loader-substituted fields.
    AB6029info / warningThe Cursor Agent version probe is unavailable (ENOENT, info) or could not complete (warning). Local pinned-schema validation still runs.
    AB6030infoThe Codex CLI is unavailable, or the installed release publishes no plugin validation command.
    AB6031info / warning (error under --strict)The Codex app-server schema-generation verb is unavailable, or its live output is missing or differs from the pinned generated hook schemas.
    AB6032errorA required Codex bundle document is missing, unreadable, invalid JSON, or fails its vendored pinned schema.
    AB6033errorA bounded Codex version or schema-generation command could not start, failed, timed out, exceeded 1 MiB of output, or produced unreadable output.

    AB6031 is the interesting one: when Codex's generated schema disagrees with the pinned revision, the pin is only updated once Codex publishes the matching contract. Following live host output automatically would turn an upstream change into a silent contract change here.

    Agent Skills spec lint

    AB6034 is an error on emitted bytes: a SKILL.md with valid YAML frontmatter but no Markdown instruction body after it. The pinned Agent Skills specification requires frontmatter followed by Markdown content, and the check runs against what the artifact actually contains rather than against the authored source.

    Hash pins versus repository-owned tables

    Hash pins cover vendored external content whose ground truth lives outside the repository and can drift: host document schemas under src/adapters/schemas/* (with upstream URL, commit, and SHA-256 recorded in PROVENANCE.json), the Agent Skills specification-derived schema pin in the manifest agentSkills block, and emitted artifact files and source inputs for integrity.

    Repository-owned capability tables and evidence are not hashed. Capability evidence records the observed host version (observedVersion) and target, while adapters carry a monotonic adapterRevision. Git already versions repository-owned content; hashing it again inside the repository is self-referential and causes churn on every table edit.

    The npm prepack gate

    npx agent-bundle prepack --root . --output artifact --json

    prepack runs the release build, creates the canonical npm root at dist/, and runs npm pack --dry-run --json --ignore-scripts from that root by launching npm's npm-cli.js through the current Node executable (process.execPath), so the gate works when npm is not a spawnable PATH command (a pnpm-managed Node, or Windows without npm.cmd on PATH). It never treats npm_execpath as npm when that variable points at pnpm. The generated package copies the validated composite artifact unchanged, points routed package.json bins at the manifest-declared bin/<name>.mjs, and adds only separately authored bins and libraries. The gate checks that exact inventory, manifest hashes, package bin targets, release-version agreement, package-only compile evidence, and the installed-dependency fields of package.json. Authored package.json.files and .npmignore rules never narrow the generated root: every selected hidden host directory and every manifest file is packed, or prepack fails before publication. The build inlines every dependency into authored package entries and the host packs, so a published plugin should install nothing: declare the framework, @agent-bundle/runtime, react, zod, and the rest of the stack under devDependencies. A compiled bundle, a host-pack module or a dist bundle alike, cannot keep a bare package external: the compiler reports AB6005 before prepack reaches the inventory. So bundling a package does not prove that consumers need it installed. --output is an artifact path relative to --root that overrides the configured output.distPath, defaulting to artifact. Run the gate directly (generated projects expose pack:check), then publish the generated root with npm publish ./dist --ignore-scripts. No npm lifecycle ever performs a host install.

    Consumer preinstall, install, and postinstall scripts are preserved only when they do not invoke Node directly, or when they are one node <generated-package-path> command. Known authored paths below the source dist/ root are rebased to the generated root and checked against staged files. Unsupported Node commands and absent source assets report AB4768; the package builder never copies the source tree to make a lifecycle path work.

    That inlining is the compiler's contract, not a bundler default a toolchain upgrade may flip. Generated executables, artifact scripts, the routed CLI, MCP entries, hook wrappers, and the package build's JavaScript bundles, compile with Rslib's autoExternal disabled, bundle: true, and splitChunks: false, and the framework adds no externals of its own; Rslib's node target leaves only Node built-ins (node:fs, path, plus Yarn PnP's pnpapi) external. MCP App views inline every script and style into one HTML file, and the same audit plugin sits in their Rsbuild compiler: a browser document has no allowable external, so any module a view's compilation kept external, a react mapped to a global through the hatch, say, fails the view with AB6005 (Compiled MCP App view "mcp-apps/<name>.html" keeps … external …). AB4725 rejects tools.rsbuild.output.autoExternal other than false and statically visible string or object externals entries that name packages. RegExp, function-form, relative, and mutator externals are judged from the compilation's own evidence: AB6005 fails a generated executable in a host pack or dist when Rspack kept anything except a built-in, pnpapi, or an emitted sibling external. An external kept through the tools hatch is therefore not a way around the contract and never reaches the prepack inventory.

    AB7014 accepts exactly three kinds of consumer-runtime evidence:

    1. runtimeDependencies on a prebuilt payload declared with definePrebuilt;
    2. a packed declaration file reference, as TypeScript's preProcessFile reads it: imports, exports, import-equals require, import("…") types, module augmentations in an external-module file, and /// <reference types> (including its @types twin); an ambient declare module in a file with no imports counts for nothing, while a #subpath follows the package imports map;
    3. a consumer-side preinstall, install, or postinstall script, including delegated scripts, that names the dependency or runs one of its bin commands.

    The framework's own runtime modules load no package at run time, so there is no framework process-dependency record to read as a fourth source.

    A package merely inlined into a compiled bundle is not used. When that is the only evidence, AB7014 names the dist bundles that inlined it, for example: The build inlined "a" into dist/bin/x.js; every consumer installs them for nothing. Move build-only packages to devDependencies; compiled bundles inline their imports (AB6005). Keep a runtime dependency only for what a packed declaration file references, a consumer install script names or runs, or a prebuilt payload declares in runtimeDependencies (definePrebuilt).

    CodeMeaning
    AB7010The dry-run npm inventory omits an expected npm-root file. Pack the generated root without excluding its files.
    AB7011A copied artifact file in the npm root no longer matches its manifest SHA-256. Rebuild, and do not modify the generated root.
    AB7012A bin in the generated npm-root or published package.json points into src/ or names a file absent from that package's packed file set. Point each bin at a packed file; routed CLIs use manifest-declared bin/<name>.mjs and authored bins use generated bin/*.js.
    AB7013package.json, normalized plugin metadata, a host manifest, or artifact provenance reports a different release version. Make every release identity agree.
    AB7014A dependencies, optionalDependencies, or peerDependencies field names packages a consumer never needs installed. Use requires one of the three evidence sources above; compiled bundles that inline a package do not count. The diagnostic names any dist bundles that inlined an otherwise unused package. It is an error for dependencies and optionalDependencies, and a warning for peerDependencies; optional peers are skipped, and an optionalDependencies entry supersedes the same name under dependencies.
    AB7015A dependencies, optionalDependencies, or peerDependencies entry a consumer's npm cannot resolve through a registry. Name and specifier are read with npm-package-arg, npm's own parser, and come out as one of three kinds: registry (a version, range, dist-tag, or npm: alias of one), fetched (a git, GitHub-shorthand, remote-tarball, or path source, npm 12 refuses git and remote fetches by default (allow-git, allow-remote), and a path never exists on the consumer's disk), or unparseable (a name npm rejects, a scheme it lacks such as link:, portal:, or a typo, a selector that is neither a range nor a URL-safe dist-tag, an alias of a non-registry target, or an invalid URL, the manifest read itself fails, so this is reported even on an optional peer). A fetched optionalDependencies entry warns because npm can continue without it, but escalates to an error when an install script needs the package: its bin command appears in command position, a node_modules/<name>/ file is run, or a bare package is preloaded with -r/--require/--import/--loader (including through NODE_OPTIONS). A peer also present in dependencies or optionalDependencies is judged by that concrete entry. The canonical root is validated for npm publish ./dist, so workspace: and catalog: are always rejected; packed bundleDependencies and installable in-package file: sources are not reported. Depend on a published version, or bundle the package and declare it under devDependencies.

    A release build also refuses a project whose package.json has no valid release version (AB4013), so a published artifact never carries the 0.0.0-dev.<short-revision> development fallback.

    Next