fix: normalize claude hooks before injection (#2001)
This commit is contained in:
parent
c633a7ac49
commit
401687c029
|
|
@ -1159,6 +1159,43 @@ describe("setupWorkspaceHooks — activity-updater (#1941)", () => {
|
|||
expect(commands).toContain(ACTIVITY_CMD_UNIX); // our hook added
|
||||
});
|
||||
|
||||
it("migrates a legacy bare SessionStart hook before adding activity-updater", async () => {
|
||||
const existingSettings = {
|
||||
hooks: {
|
||||
SessionStart: [
|
||||
{
|
||||
type: "command",
|
||||
command: "python .claude/hooks/session_start_context.py",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
mockExistsSync.mockReturnValueOnce(true);
|
||||
mockReadFile.mockResolvedValueOnce(JSON.stringify(existingSettings));
|
||||
mockWriteFile.mockClear();
|
||||
|
||||
await agent.setupWorkspaceHooks!("/workspace/test", {} as WorkspaceHooksConfig);
|
||||
|
||||
const settings = getParsedSettings();
|
||||
const sessionStart = (settings.hooks as Record<string, unknown>)["SessionStart"] as Array<{
|
||||
matcher: string;
|
||||
hooks: Array<{ command: string }>;
|
||||
}>;
|
||||
expect(sessionStart[0]).toEqual({
|
||||
matcher: "",
|
||||
hooks: [
|
||||
{
|
||||
type: "command",
|
||||
command: "python .claude/hooks/session_start_context.py",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const commands = sessionStart.flatMap((g) => g.hooks).map((h) => h.command);
|
||||
expect(commands).toContain("python .claude/hooks/session_start_context.py");
|
||||
expect(commands).toContain(ACTIVITY_CMD_UNIX);
|
||||
});
|
||||
|
||||
it("tolerates malformed hooks.<event> (object instead of array)", async () => {
|
||||
// A user could hand-edit settings.json or an older plugin could have
|
||||
// written a non-array shape there. We must not crash — start fresh.
|
||||
|
|
|
|||
|
|
@ -810,6 +810,36 @@ interface HookRegistration {
|
|||
identifiers: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isBareHookDefinition(value: unknown): boolean {
|
||||
if (!isRecord(value)) return false;
|
||||
return typeof value["command"] === "string" && !Array.isArray(value["hooks"]);
|
||||
}
|
||||
|
||||
function normalizeHookEntry(entry: unknown): unknown {
|
||||
if (!isRecord(entry)) return entry;
|
||||
|
||||
if (isBareHookDefinition(entry)) {
|
||||
return { matcher: "", hooks: [entry] };
|
||||
}
|
||||
|
||||
if (Array.isArray(entry["hooks"]) && typeof entry["matcher"] !== "string") {
|
||||
return { ...entry, matcher: "" };
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
function normalizeClaudeHookSettings(hooks: Record<string, unknown>): void {
|
||||
for (const [event, entries] of Object.entries(hooks)) {
|
||||
if (!Array.isArray(entries)) continue;
|
||||
hooks[event] = entries.map(normalizeHookEntry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the registration's hook in the `event`'s hook array, updating any
|
||||
* existing entry whose command contains one of `identifiers` (idempotent).
|
||||
|
|
@ -835,13 +865,13 @@ function upsertHookEntry(
|
|||
let foundDefIdx = -1;
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
||||
const hooksList = (entry as Record<string, unknown>)["hooks"];
|
||||
if (!isRecord(entry)) continue;
|
||||
const hooksList = entry["hooks"];
|
||||
if (!Array.isArray(hooksList)) continue;
|
||||
for (let j = 0; j < hooksList.length; j++) {
|
||||
const def = hooksList[j];
|
||||
if (typeof def !== "object" || def === null || Array.isArray(def)) continue;
|
||||
const cmd = (def as Record<string, unknown>)["command"];
|
||||
if (!isRecord(def)) continue;
|
||||
const cmd = def["command"];
|
||||
if (typeof cmd === "string" && reg.identifiers.some((id) => cmd.includes(id))) {
|
||||
foundEntryIdx = i;
|
||||
foundDefIdx = j;
|
||||
|
|
@ -989,7 +1019,8 @@ async function setupHookInWorkspace(workspacePath: string): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
const hooks = (existingSettings["hooks"] as Record<string, unknown>) ?? {};
|
||||
const hooks = isRecord(existingSettings["hooks"]) ? existingSettings["hooks"] : {};
|
||||
normalizeClaudeHookSettings(hooks);
|
||||
for (const reg of buildHookRegistrations(metadataCommand, activityCommand)) {
|
||||
upsertHookEntry(hooks, reg);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue