Plugins: wire Claude bundle hook resolution (parity with Codex)

This commit is contained in:
Vincent Koc 2026-03-17 23:12:00 -07:00
parent d1d10007a9
commit b9b891b614
2 changed files with 66 additions and 2 deletions

View File

@ -113,7 +113,7 @@ describe("bundle manifest parsing", () => {
bundleFormat: "claude",
skills: ["skill-packs/starter", "commands-pack"],
settingsFiles: ["settings.json"],
hooks: [],
hooks: ["hooks/hooks.json", "hooks-pack"],
capabilities: expect.arrayContaining([
"hooks",
"skills",
@ -191,6 +191,70 @@ describe("bundle manifest parsing", () => {
);
});
it("resolves Claude bundle hooks from default and declared paths", () => {
const rootDir = makeTempDir();
mkdirSafe(path.join(rootDir, ".claude-plugin"));
mkdirSafe(path.join(rootDir, "hooks"));
fs.writeFileSync(path.join(rootDir, "hooks", "hooks.json"), '{"hooks":[]}', "utf-8");
fs.writeFileSync(
path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH),
JSON.stringify({
name: "Hook Plugin",
description: "Claude hooks fixture",
}),
"utf-8",
);
const result = loadBundleManifest({ rootDir, bundleFormat: "claude" });
expect(result.ok).toBe(true);
if (!result.ok) {
return;
}
expect(result.manifest.hooks).toEqual(["hooks/hooks.json"]);
expect(result.manifest.capabilities).toContain("hooks");
});
it("resolves Claude bundle hooks from manifest-declared paths only", () => {
const rootDir = makeTempDir();
mkdirSafe(path.join(rootDir, ".claude-plugin"));
mkdirSafe(path.join(rootDir, "custom-hooks"));
fs.writeFileSync(
path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH),
JSON.stringify({
name: "Custom Hook Plugin",
hooks: "custom-hooks",
}),
"utf-8",
);
const result = loadBundleManifest({ rootDir, bundleFormat: "claude" });
expect(result.ok).toBe(true);
if (!result.ok) {
return;
}
expect(result.manifest.hooks).toEqual(["custom-hooks"]);
expect(result.manifest.capabilities).toContain("hooks");
});
it("returns empty hooks for Claude bundles with no hooks directory", () => {
const rootDir = makeTempDir();
mkdirSafe(path.join(rootDir, ".claude-plugin"));
mkdirSafe(path.join(rootDir, "skills"));
fs.writeFileSync(
path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH),
JSON.stringify({ name: "No Hooks" }),
"utf-8",
);
const result = loadBundleManifest({ rootDir, bundleFormat: "claude" });
expect(result.ok).toBe(true);
if (!result.ok) {
return;
}
expect(result.manifest.hooks).toEqual([]);
expect(result.manifest.capabilities).not.toContain("hooks");
});
it("does not misclassify native index plugins as manifestless Claude bundles", () => {
const rootDir = makeTempDir();
mkdirSafe(path.join(rootDir, "commands"));

View File

@ -397,7 +397,7 @@ export function loadBundleManifest(params: {
version,
skills: resolveClaudeSkillDirs(raw, params.rootDir),
settingsFiles: resolveClaudeSettingsFiles(raw, params.rootDir),
hooks: [],
hooks: resolveClaudeHookPaths(raw, params.rootDir),
bundleFormat: "claude",
capabilities: buildClaudeCapabilities(raw, params.rootDir),
},