From 1887ada215b27bb1025d9b7696a46ae7a4ac0f7a Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 20 Jul 2026 13:43:08 +0200 Subject: [PATCH] fix: Silence missing PostToolUse callbacks (#895) Closes #889 --- src/acp-agent.ts | 2 +- src/tests/tools.test.ts | 46 ++++++++++++++++++++++++++++++++++------- src/tools.ts | 13 ++---------- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/acp-agent.ts b/src/acp-agent.ts index bec6c011..7b703154 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -5081,7 +5081,7 @@ export class ClaudeAcpAgent { ...(userProvidedOptions?.hooks?.PostToolUse || []), { hooks: [ - createPostToolUseHook(this.logger, { + createPostToolUseHook({ onEnterPlanMode: async () => { await this.client.sessionUpdate({ sessionId, diff --git a/src/tests/tools.test.ts b/src/tests/tools.test.ts index d2adff7a..b4961596 100644 --- a/src/tests/tools.test.ts +++ b/src/tests/tools.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect, beforeEach, vi } from "vitest"; import { ClientCapabilities } from "@agentclientprotocol/sdk"; import { ImageBlockParam, ToolResultBlockParam } from "@anthropic-ai/sdk/resources"; import { @@ -1024,7 +1024,7 @@ describe("Bash terminal output", () => { ); // Fire PostToolUse hook with a structuredPatch in tool_response - const hook = createPostToolUseHook(mockLogger); + const hook = createPostToolUseHook(); await hook( { hook_event_name: "PostToolUse", @@ -1102,7 +1102,7 @@ describe("Bash terminal output", () => { mockLogger, ); - const hook = createPostToolUseHook(mockLogger); + const hook = createPostToolUseHook(); await hook( { hook_event_name: "PostToolUse", @@ -1182,7 +1182,7 @@ describe("Bash terminal output", () => { mockLogger, ); - const hook = createPostToolUseHook(mockLogger); + const hook = createPostToolUseHook(); await hook( { hook_event_name: "PostToolUse", @@ -1203,6 +1203,36 @@ describe("Bash terminal output", () => { expect(hookUpdate.content).toBeUndefined(); expect(hookUpdate.locations).toBeUndefined(); }); + + // Regression for issue #889: tool uses that never register a callback + // (TodoWrite/Task* are rendered as plan updates, not tool_calls) fire the + // PostToolUse hook too. That's expected — the hook must stay silent + // instead of spamming "No onPostToolUseHook found" to stderr. + it("should not log an error when no callback is registered for the tool use", async () => { + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + const hook = createPostToolUseHook(); + const result = await hook( + { + hook_event_name: "PostToolUse", + tool_name: "TodoWrite", + tool_input: { todos: [] }, + tool_response: { success: true }, + tool_use_id: "toolu_todo_no_callback", + session_id: "test-session", + transcript_path: "/tmp/test", + cwd: "/tmp", + }, + "toolu_todo_no_callback", + { signal: AbortSignal.abort() }, + ); + + expect(result).toEqual({ continue: true }); + expect(errorSpy).not.toHaveBeenCalled(); + } finally { + errorSpy.mockRestore(); + } + }); }); describe("post-tool-use hook sends diff content for Write tool", () => { @@ -1239,7 +1269,7 @@ describe("Bash terminal output", () => { mockLogger, ); - const hook = createPostToolUseHook(mockLogger); + const hook = createPostToolUseHook(); await hook( { hook_event_name: "PostToolUse", @@ -1315,7 +1345,7 @@ describe("Bash terminal output", () => { mockLogger, ); - const hook = createPostToolUseHook(mockLogger); + const hook = createPostToolUseHook(); await hook( { hook_event_name: "PostToolUse", @@ -1438,7 +1468,7 @@ describe("Bash terminal output", () => { expect((resultNotifications[1].update as any).status).toBe("completed"); // Step 3: Fire the PostToolUse hook (simulates what Claude Code SDK does) - const hook = createPostToolUseHook(mockLogger); + const hook = createPostToolUseHook(); await hook( { hook_event_name: "PostToolUse", @@ -1519,7 +1549,7 @@ describe("Bash terminal output", () => { ); // Fire hook - const hook = createPostToolUseHook(mockLogger); + const hook = createPostToolUseHook(); await hook( { hook_event_name: "PostToolUse", diff --git a/src/tools.ts b/src/tools.ts index 199b3473..9980187b 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -60,7 +60,6 @@ import { BetaWebSearchToolResultBlockParam, } from "@anthropic-ai/sdk/resources/beta.mjs"; import path from "node:path"; -import { Logger } from "./acp-agent.js"; /** * Union of all possible content types that can appear in tool results from the Anthropic SDK. @@ -1217,12 +1216,7 @@ export const registerHookCallback = ( /* A callback for Claude Code that is called when receiving a PostToolUse hook */ export const createPostToolUseHook = - ( - logger: Logger = console, - options?: { - onEnterPlanMode?: () => Promise; - }, - ): HookCallback => + (options?: { onEnterPlanMode?: () => Promise }): HookCallback => async (input: any, toolUseID: string | undefined): Promise<{ continue: boolean }> => { if (input.hook_event_name === "PostToolUse") { // Handle EnterPlanMode tool - notify client of mode change after successful execution @@ -1234,11 +1228,8 @@ export const createPostToolUseHook = const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook; if (onPostToolUseHook) { await onPostToolUseHook(toolUseID, input.tool_input, input.tool_response); - delete toolUseCallbacks[toolUseID]; // Cleanup after execution - } else { - logger.error(`No onPostToolUseHook found for tool use ID: ${toolUseID}`); - delete toolUseCallbacks[toolUseID]; } + delete toolUseCallbacks[toolUseID]; // Cleanup after execution } } return { continue: true };