fix: Silence missing PostToolUse callbacks (#895)

Closes #889
This commit is contained in:
Ben Brandt
2026-07-20 13:43:08 +02:00
committed by GitHub
parent 06c3d7bdbd
commit 1887ada215
3 changed files with 41 additions and 20 deletions

View File

@@ -5081,7 +5081,7 @@ export class ClaudeAcpAgent {
...(userProvidedOptions?.hooks?.PostToolUse || []),
{
hooks: [
createPostToolUseHook(this.logger, {
createPostToolUseHook({
onEnterPlanMode: async () => {
await this.client.sessionUpdate({
sessionId,

View File

@@ -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",

View File

@@ -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<void>;
},
): HookCallback =>
(options?: { onEnterPlanMode?: () => Promise<void> }): 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 };