Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/claude-host-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": minor
---

Validate built Claude bundles with Claude Code's strict plugin developer tools and expose the bounded validator as an opt-in test helper.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ The same config also owns the npm package build — no second bundler config, bi
- `hooks list` / `hooks simulate` — inspect and simulate generated hooks
- `eval` — run eval suites against a built artifact

When validating a built `claude` or unified `plugin` target, Agent Bundle uses the installed
Claude Code developer toolchain in addition to its pinned schemas. Use
`agent-bundle validate --artifact dist --strict` in CI; Claude's `--strict` findings remain
warnings locally unless Agent Bundle strict mode is requested. If `claude` is absent, validation
reports an explicit informational skip. For the install-free development loop, run
`claude --plugin-dir dist/claude plugin list --json` after building.

The [package README](packages/agent-bundle/README.md) is the full reference: configuration semantics, the workbench, the optional Agent API, evals, and limitations.

## Examples
Expand Down
20 changes: 20 additions & 0 deletions packages/agent-bundle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,26 @@ manifests at files inside those payloads without compiling them. Payload files c

`validate --artifact`, `mcp`, and `hooks` work against a built artifact with project sources deleted.

### Validate Claude bundles with Claude Code

Artifact validation runs `claude plugin validate <bundle-dir> --strict` for emitted `claude`
and unified `plugin` targets when Claude Code is on `PATH`. Host errors become Agent Bundle
errors; host warnings remain warnings unless `agent-bundle validate --strict` is set. A missing
binary is reported as an explicit informational skip, never as fabricated success. Use
`--no-host-validation` when a deterministic schema-only check is required.

CI should use strict validation:

```sh
agent-bundle validate --artifact dist --strict
```

During development, load a built target without installing it and verify registration:

```sh
claude --plugin-dir dist/claude plugin list --json
```

## Developer workbench

`agent-bundle dev` serves a loopback-only prebuilt workbench. It shows project
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@
},
"observedCliVersion": "2.1.250",
"plugin": {
"devtools": {
"details": true,
"listJson": true,
"pluginDir": true,
"strictValidation": true,
"tag": true,
"validate": true
},
"lsp": {
"config": ".lsp.json",
"manifestField": "lspServers",
Expand Down Expand Up @@ -81,7 +89,10 @@
"First-registered-wins collision rule: when more than one enabled server declares the same extension in extensionToLanguage, from one plugin or from different plugins, the first registered handles the extension and the others never start.",
"The server binary is never vendored: \"You must install the language server binary separately. LSP plugins configure how Claude Code connects to a language server, but they don't include the server itself.\"",
"Placeholder substitution for LSP servers is limited to command, args, env, and workspaceFolder.",
"Codex and Cursor publish no plugin LSP surface at their pinned revisions, so the unified bundle's .lsp.json reaches Claude Code only."
"Codex and Cursor publish no plugin LSP surface at their pinned revisions, so the unified bundle's .lsp.json reaches Claude Code only.",
"Plugin developer tools reference: `claude plugin validate <dir>` checks plugin.json, hooks/hooks.json, and default-directory Skill, agent, and command frontmatter; manifest-less component directories require 2.1.233 or later.",
"`claude plugin validate <dir> --strict` promotes tolerated warnings such as unrecognized or near-miss fields and non-object experimental/metadata values to exit failure; the reference recommends strict mode in CI.",
"Development tools include `claude --plugin-dir <dir> plugin list --json` for registration proof, `claude plugin details <name>` for component inventory and host-owned token estimates, `claude plugin tag`, and `claude --debug` for loading diagnostics."
]
}
}
2 changes: 1 addition & 1 deletion packages/agent-bundle/src/adapters/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ const hookContract = Object.freeze({
const metadata = Object.freeze({
adapterRevision: '1.2.0',
capabilityRevision: capabilityTable.observedCliVersion,
capabilitySha256: '5beb395c075c22a290a70f76b1670fab82b16d933af37cda89da850dbd8d483c',
capabilitySha256: '13bc41224c5343b33d259986a66feb279e15431c5019bb2a1c443eaa60e9a9ea',
observedVersion: capabilityTable.observedCliVersion,
schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,15 @@
"url": "https://docs.anthropic.com/en/docs/claude-code/plugins"
}
},
"validation": "Pinned JSON Schema snapshots are validated locally with Ajv. Package builds do not download schemas or invoke host-side validators."
"validation": "Pinned JSON Schema snapshots are validated locally with Ajv. Artifact validation can additionally invoke `claude plugin validate <bundle-dir> --strict`; Agent Bundle preserves host warnings unless its own strict option is enabled, and reports an explicit unavailable diagnostic when the CLI is absent.",
"developerTools": {
"retrievedAt": "2026-09-01",
"source": "https://code.claude.com/docs/en/plugins-reference",
"evidence": [
"`claude plugin validate <dir>` validates plugin.json, hooks/hooks.json, and frontmatter in default skills, agents, and commands directories; manifest-less directories require Claude Code 2.1.233 or later.",
"`--strict` treats warnings as errors and is recommended for CI, including unrecognized or near-miss fields and non-object experimental/metadata values.",
"`claude --plugin-dir <dir> plugin list` includes session-loaded plugins only when the same flag precedes the subcommand; `--json` provides machine-readable registration evidence.",
"`claude plugin details` owns component inventory and token-cost estimation; Agent Bundle does not reimplement its count_tokens or character-fallback costing."
]
}
}
34 changes: 33 additions & 1 deletion packages/agent-bundle/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,15 @@ export {
} from './build/manifest.ts';
import { composeBundlerInspection, type BundlerInspection } from './build/inspect-bundler.ts';
export type { BundlerInspection, BundlerInspectionEntry } from './build/inspect-bundler.ts';
import { validateArtifact } from './build/validate-artifact.ts';
import { validateArtifact, validateArtifactWithSnapshot } from './build/validate-artifact.ts';
import { freezeDiagnostics, hasErrors, DiagnosticError, type Diagnostic } from './core/diagnostics.ts';
export type { Diagnostic, DiagnosticSeverity } from './core/diagnostics.ts';
import type { ProjectContext } from './core/project-context.ts';
import type { NormalizedPlugin } from './core/types.ts';
import {
validateClaudePlugin,
type ClaudePluginValidationReport,
} from './host-contracts/claude-plugin-validation.ts';
import type { EvalComparison } from './eval/compare.ts';
import { EvalRunStoreError } from './eval/errors.ts';
import {
Expand Down Expand Up @@ -214,10 +218,15 @@ export interface ProjectOptions {

export interface ValidateOptions extends ProjectOptions {
readonly artifact?: string;
/** Run installed host developer tools for compatible built targets. */
readonly hostValidation?: boolean;
/** Promote host-tool warnings to errors. */
readonly strict?: boolean;
}

export interface ValidateResult {
readonly diagnostics: readonly Diagnostic[];
readonly hostValidation?: readonly ClaudePluginValidationReport[];
readonly model?: NormalizedPlugin;
}

Expand Down Expand Up @@ -408,6 +417,29 @@ export const validate = async (options: ValidateOptions): Promise<ValidateResult
if (options.artifact !== undefined) {
const artifact = resolve(options.artifact);
log(options.logger, 'artifact.validate', { artifact });
if (options.hostValidation === true) {
const validated = await validateArtifactWithSnapshot({
artifactRoot: artifact,
registry: registryFor(options),
});
if (validated.snapshot === undefined) {
return Object.freeze({ diagnostics: freezeDiagnostics(validated.diagnostics) });
}
const reports = await Promise.all(validated.snapshot.manifest.targets
.filter((target) => target.name === 'claude' || target.name === 'plugin')
.map((target) => validateClaudePlugin({
pluginDirectory: join(artifact, target.name),
strict: options.strict,
target: target.name,
})));
return Object.freeze({
diagnostics: freezeDiagnostics([
...validated.diagnostics,
...reports.flatMap((report) => report.diagnostics),
]),
...(reports.length === 0 ? {} : { hostValidation: Object.freeze(reports) }),
});
}
return Object.freeze({
diagnostics: freezeDiagnostics(await validateArtifact({ artifactRoot: artifact, registry: registryFor(options) })),
});
Expand Down
10 changes: 9 additions & 1 deletion packages/agent-bundle/src/build/artifact-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ export type ArtifactDiagnosticCode =
| 'AB6015'
| 'AB6016'
| 'AB6017'
| 'AB6018';
| 'AB6018'
| 'AB6019'
| 'AB6020'
| 'AB6021'
| 'AB6022';

export const artifactDiagnosticRecoveries: Readonly<Record<ArtifactDiagnosticCode, string>> = Object.freeze({
AB6000: 'Restore a readable artifact root and canonical manifest, then rebuild the artifact.',
Expand All @@ -41,6 +45,10 @@ export const artifactDiagnosticRecoveries: Readonly<Record<ArtifactDiagnosticCod
AB6016: 'Copy every referenced Skill resource inside its Skill root, then rebuild the artifact.',
AB6017: 'Rebuild the artifact so every target MCP manifest references its exact compiler outputs.',
AB6018: 'Rebuild the artifact so native hook commands and hook metadata agree.',
AB6019: 'Install Claude Code and ensure `claude` is on PATH, then rerun artifact validation.',
AB6020: 'Run `claude plugin validate <bundle-dir> --strict`, repair the warning, and rebuild.',
AB6021: 'Run `claude plugin validate <bundle-dir> --strict`, repair the error, and rebuild.',
AB6022: 'Restore a bounded Claude validator process, then rerun artifact validation.',
});

const isArtifactDiagnosticCode = (code: string): code is ArtifactDiagnosticCode =>
Expand Down
17 changes: 15 additions & 2 deletions packages/agent-bundle/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ export interface CliDependencies {
/** Injectable only to make foreground shutdown behavior deterministic in tests. */
readonly signals?: CliSignalSource;
readonly startDevServer?: typeof startDevServer;
/** Injectable only to verify host-validation CLI policy without an installed host. */
readonly validate?: typeof validate;
}

interface SourceCommandOptions {
readonly config?: string;
readonly hostValidation?: boolean;
readonly json?: boolean;
readonly mode?: string;
readonly root: string;
readonly strict?: boolean;
readonly target?: readonly string[];
}

Expand Down Expand Up @@ -365,10 +369,19 @@ export const runCli = async (

const validateCommand = configureSourceOptions(
program.command('validate').description('Validate project source or one artifact'),
).option('--artifact <path>', 'Validate exactly this built artifact');
)
.option('--artifact <path>', 'Validate exactly this built artifact')
.option('--host-validation', 'Run installed host developer tools for compatible built targets', true)
.option('--no-host-validation', 'Skip installed host developer tools')
.option('--strict', 'Promote host-tool warnings to errors');
validateCommand.action(async (options: SourceCommandOptions & { readonly artifact?: string }) => {
const { validate } = await import('./api.ts');
const result = await validate({ ...projectOptions(options), artifact: options.artifact });
const result = await (dependencies.validate ?? validate)({
...projectOptions(options),
artifact: options.artifact,
hostValidation: options.hostValidation,
strict: options.strict,
});
if (result.diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
throw new DiagnosticError(result.diagnostics);
}
Expand Down
Loading
Loading