diff --git a/packages/server/src/server/agent/agent-response-loop.real.e2e.test.ts b/packages/server/src/server/agent/agent-response-loop.real.e2e.test.ts index ec9dbfdb10..a92e547edc 100644 --- a/packages/server/src/server/agent/agent-response-loop.real.e2e.test.ts +++ b/packages/server/src/server/agent/agent-response-loop.real.e2e.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest"; import { createServer } from "http"; -import { mkdtempSync, rmSync } from "fs"; +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import path from "path"; import { randomUUID } from "crypto"; @@ -250,4 +250,39 @@ describe("getStructuredAgentResponse (e2e)", () => { expect(result.message.trim().toLowerCase()).toBe("hello"); }, 180000); + + test("does not run the project's Claude hooks for an internal Claude agent", async (context) => { + if (!canRunClaude) { + context.skip(); + } + const hookMarker = path.join(cwd, "stop-hook-ran"); + mkdirSync(path.join(cwd, ".claude")); + writeFileSync( + path.join(cwd, ".claude", "settings.json"), + JSON.stringify({ + hooks: { + Stop: [{ hooks: [{ type: "command", command: `touch '${hookMarker}'` }] }], + }, + }), + ); + + const result = await generateStructuredAgentResponse({ + manager, + agentConfig: { + provider: "claude", + model: CLAUDE_TEST_MODEL, + cwd, + title: "Branch name generator", + internal: true, + }, + prompt: + 'Respond with exactly this JSON (no markdown, no extra keys, no extra text): {"message":"hello"}', + schema: z.object({ message: z.string() }), + maxRetries: 2, + persistSession: false, + }); + + expect(result.message.trim().toLowerCase()).toBe("hello"); + expect(existsSync(hookMarker)).toBe(false); + }, 180000); }); diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index 16aa193cce..3e91b9d639 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -1067,6 +1067,35 @@ describe("ClaudeAgentSession features", () => { await session.close(); }); + test("disables Claude hooks for internal agents only", async () => { + const { queryFactory } = createQueryMock(); + const client = new ClaudeAgentClient({ + logger, + queryFactory, + resolveBinary: async () => "/test/claude/bin", + }); + const internalSession = await client.createSession({ + provider: "claude", + cwd: process.cwd(), + internal: true, + }); + const userSession = await client.createSession({ + provider: "claude", + cwd: process.cwd(), + }); + + await internalSession.startTurn("hello"); + await userSession.startTurn("hello"); + + expect(queryFactory.mock.calls[0]?.[0].options.settings).toMatchObject({ + disableAllHooks: true, + }); + expect(queryFactory.mock.calls[1]?.[0].options.settings).toBeUndefined(); + + await internalSession.close(); + await userSession.close(); + }); + test("turns Claude thinking off without retaining an effort level", async () => { const { queryFactory, launches } = createQueryMock(); const client = new ClaudeAgentClient({ diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 38476490b3..7eecf56c96 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -3363,13 +3363,17 @@ class ClaudeAgentSession implements AgentSession { input: { ultracode: boolean }, ): Pick | Record { const fastMode = this.resolveFastModeSetting(); - if (fastMode === null && !input.ultracode) { + // Internal agents do daemon work such as naming a branch, so the user's and + // project's hooks must not run for them. + const disableAllHooks = this.config.internal === true; + if (fastMode === null && !input.ultracode && !disableAllHooks) { return {}; } return { settings: mergeClaudeSettings(providerOptions.settings, { ...(fastMode === null ? {} : { fastMode }), ...(input.ultracode ? { ultracode: true } : {}), + ...(disableAllHooks ? { disableAllHooks: true } : {}), }), }; }