From 479277efd27b17dcffabeb9ab554589ae99608fc Mon Sep 17 00:00:00 2001 From: nikhil achale Date: Sat, 23 May 2026 11:48:46 +0530 Subject: [PATCH] fix: preserve claude hook matchers during normalization (#2001) --- .../agent-claude-code/src/index.test.ts | 58 +++++++++++++++++++ .../plugins/agent-claude-code/src/index.ts | 3 +- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/plugins/agent-claude-code/src/index.test.ts b/packages/plugins/agent-claude-code/src/index.test.ts index 623bc60ad..2b3954559 100644 --- a/packages/plugins/agent-claude-code/src/index.test.ts +++ b/packages/plugins/agent-claude-code/src/index.test.ts @@ -1196,6 +1196,64 @@ describe("setupWorkspaceHooks — activity-updater (#1941)", () => { expect(commands).toContain(ACTIVITY_CMD_UNIX); }); + it("preserves matcher from a malformed bare hook entry while wrapping it", async () => { + const existingSettings = { + hooks: { + PostToolUse: [ + { + matcher: "Bash", + type: "command", + command: "echo user-bash-hook", + }, + ], + }, + }; + mockExistsSync.mockReturnValueOnce(true); + mockReadFile.mockResolvedValueOnce(JSON.stringify(existingSettings)); + mockWriteFile.mockClear(); + + await agent.setupWorkspaceHooks!("/workspace/test", {} as WorkspaceHooksConfig); + + const settings = getParsedSettings(); + const postToolUse = (settings.hooks as Record)["PostToolUse"] as Array<{ + matcher: string; + hooks: Array<{ command: string }>; + }>; + const userEntry = postToolUse.find((g) => + g.hooks.some((h) => h.command === "echo user-bash-hook"), + ); + expect(userEntry?.matcher).toBe("Bash"); + }); + + it("adds an empty matcher to hook groups that are missing one", async () => { + const existingSettings = { + hooks: { + UserPromptSubmit: [ + { + hooks: [{ type: "command", command: "echo user-prompt-hook" }], + }, + ], + }, + }; + mockExistsSync.mockReturnValueOnce(true); + mockReadFile.mockResolvedValueOnce(JSON.stringify(existingSettings)); + mockWriteFile.mockClear(); + + await agent.setupWorkspaceHooks!("/workspace/test", {} as WorkspaceHooksConfig); + + const settings = getParsedSettings(); + const userPromptSubmit = (settings.hooks as Record)[ + "UserPromptSubmit" + ] as Array<{ + matcher: string; + hooks: Array<{ command: string }>; + }>; + const userEntry = userPromptSubmit.find((g) => + g.hooks.some((h) => h.command === "echo user-prompt-hook"), + ); + expect(userEntry?.matcher).toBe(""); + }); + it("tolerates malformed hooks. (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. diff --git a/packages/plugins/agent-claude-code/src/index.ts b/packages/plugins/agent-claude-code/src/index.ts index 9954403b6..2e148648a 100644 --- a/packages/plugins/agent-claude-code/src/index.ts +++ b/packages/plugins/agent-claude-code/src/index.ts @@ -823,7 +823,8 @@ function normalizeHookEntry(entry: unknown): unknown { if (!isRecord(entry)) return entry; if (isBareHookDefinition(entry)) { - return { matcher: "", hooks: [entry] }; + const inheritedMatcher = typeof entry["matcher"] === "string" ? entry["matcher"] : ""; + return { matcher: inheritedMatcher, hooks: [entry] }; } if (Array.isArray(entry["hooks"]) && typeof entry["matcher"] !== "string") {