diff --git a/src/hooks/claude-code-hooks/user-prompt-submit.test.ts b/src/hooks/claude-code-hooks/user-prompt-submit.test.ts new file mode 100644 index 000000000..334164fbe --- /dev/null +++ b/src/hooks/claude-code-hooks/user-prompt-submit.test.ts @@ -0,0 +1,107 @@ +import { describe, it, expect } from "bun:test" +import { + executeUserPromptSubmitHooks, + type UserPromptSubmitContext, +} from "./user-prompt-submit" + +describe("executeUserPromptSubmitHooks", () => { + it("returns early when no config provided", async () => { + // given + const ctx: UserPromptSubmitContext = { + sessionId: "test-session", + prompt: "test prompt", + parts: [{ type: "text", text: "test prompt" }], + cwd: "/tmp", + } + + // when + const result = await executeUserPromptSubmitHooks(ctx, null) + + // then + expect(result.block).toBe(false) + expect(result.messages).toEqual([]) + }) + + it("returns early when hook tags present in user input", async () => { + // given + const ctx: UserPromptSubmitContext = { + sessionId: "test-session", + prompt: "previous output", + parts: [ + { + type: "text", + text: "previous output", + }, + ], + cwd: "/tmp", + } + + // when + const result = await executeUserPromptSubmitHooks(ctx, null) + + // then + expect(result.block).toBe(false) + expect(result.messages).toEqual([]) + }) + + it("does not return early when hook tags in prompt but not in user input", async () => { + // given - simulates case where hook output was injected into session context + // but current user input does not contain tags + const ctx: UserPromptSubmitContext = { + sessionId: "test-session", + prompt: + "previous output\n\nuser message", + parts: [{ type: "text", text: "user message" }], + cwd: "/tmp", + } + + // when + const result = await executeUserPromptSubmitHooks(ctx, null) + + // then - should not return early, should continue to config check + expect(result.block).toBe(false) + expect(result.messages).toEqual([]) + }) + + it("should fire on first prompt", async () => { + // given + const ctx: UserPromptSubmitContext = { + sessionId: "test-session-1", + prompt: "first prompt", + parts: [{ type: "text", text: "first prompt" }], + cwd: "/tmp", + } + + // when + const result = await executeUserPromptSubmitHooks(ctx, null) + + // then + expect(result.block).toBe(false) + expect(result.messages).toEqual([]) + }) + + it("should fire on second prompt in same session", async () => { + // given + const ctx1: UserPromptSubmitContext = { + sessionId: "test-session-2", + prompt: "first prompt", + parts: [{ type: "text", text: "first prompt" }], + cwd: "/tmp", + } + + const ctx2: UserPromptSubmitContext = { + sessionId: "test-session-2", + prompt: "second prompt", + parts: [{ type: "text", text: "second prompt" }], + cwd: "/tmp", + } + + // when + const result1 = await executeUserPromptSubmitHooks(ctx1, null) + const result2 = await executeUserPromptSubmitHooks(ctx2, null) + + // then + expect(result1.block).toBe(false) + expect(result2.block).toBe(false) + }) +}) diff --git a/src/hooks/claude-code-hooks/user-prompt-submit.ts b/src/hooks/claude-code-hooks/user-prompt-submit.ts index b358ef392..80f1b0bf6 100644 --- a/src/hooks/claude-code-hooks/user-prompt-submit.ts +++ b/src/hooks/claude-code-hooks/user-prompt-submit.ts @@ -44,9 +44,16 @@ export async function executeUserPromptSubmitHooks( return { block: false, modifiedParts, messages } } + // Check if hook tags are in the current user input only (not in injected context) + // by checking only the text parts that were provided in this message + const userInputText = ctx.parts + .filter((p) => p.type === "text" && p.text) + .map((p) => p.text ?? "") + .join("\n") + if ( - ctx.prompt.includes(USER_PROMPT_SUBMIT_TAG_OPEN) && - ctx.prompt.includes(USER_PROMPT_SUBMIT_TAG_CLOSE) + userInputText.includes(USER_PROMPT_SUBMIT_TAG_OPEN) && + userInputText.includes(USER_PROMPT_SUBMIT_TAG_CLOSE) ) { return { block: false, modifiedParts, messages } }