Skip to content

Commit 96b73a0

Browse files
Add logging hooks (microsoft#2894)
So we can understand what claude harness is doing
1 parent 06237e2 commit 96b73a0

File tree

7 files changed

+409
-15
lines changed

7 files changed

+409
-15
lines changed

src/extension/agents/claude/node/claudeCodeAgent.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { HookInput, HookJSONOutput, Options, PreToolUseHookInput, Query, SDKAssistantMessage, SDKResultMessage, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
6+
import { HookCallbackMatcher, HookEvent, HookInput, HookJSONOutput, Options, PreToolUseHookInput, Query, SDKAssistantMessage, SDKResultMessage, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
77
import Anthropic from '@anthropic-ai/sdk';
88
import type * as vscode from 'vscode';
99
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
@@ -26,6 +26,7 @@ import { claudeEditTools, ClaudeToolNames, getAffectedUrisForEditTool, IExitPlan
2626
import { createFormattedToolInvocation } from '../common/toolInvocationFormatter';
2727
import { IClaudeCodeSdkService } from './claudeCodeSdkService';
2828
import { ClaudeLanguageModelServer, IClaudeLanguageModelServerConfig } from './claudeLanguageModelServer';
29+
import { buildHooksFromRegistry } from './hooks/index';
2930

3031
// Manages Claude Code agent interactions and language model server lifecycle
3132
export class ClaudeAgentManager extends Disposable {
@@ -287,20 +288,7 @@ export class ClaudeCodeSession extends Disposable {
287288
resume: this.sessionId,
288289
// Pass the model selection to the SDK
289290
...(this._currentModelId !== undefined ? { model: this._currentModelId } : {}),
290-
hooks: {
291-
PreToolUse: [
292-
{
293-
matcher: claudeEditTools.join('|'),
294-
hooks: [(input, toolID) => this._onWillEditTool(input, toolID, token)]
295-
}
296-
],
297-
PostToolUse: [
298-
{
299-
matcher: claudeEditTools.join('|'),
300-
hooks: [(input, toolID) => this._onDidEditTool(input, toolID)]
301-
}
302-
],
303-
},
291+
hooks: this._buildHooks(token),
304292
canUseTool: async (name, input) => {
305293
return this._currentRequest ?
306294
this.canUseTool(name, input, this._currentRequest.toolInvocationToken) :
@@ -328,6 +316,32 @@ export class ClaudeCodeSession extends Disposable {
328316
this._processMessages();
329317
}
330318

319+
/**
320+
* Builds the hooks configuration by combining registry-based hooks with edit tool hooks.
321+
*/
322+
private _buildHooks(token: CancellationToken): Partial<Record<HookEvent, HookCallbackMatcher[]>> {
323+
const hooks = buildHooksFromRegistry(this.instantiationService);
324+
325+
// Add edit tool hooks to PreToolUse and PostToolUse
326+
if (!hooks.PreToolUse) {
327+
hooks.PreToolUse = [];
328+
}
329+
hooks.PreToolUse.push({
330+
matcher: claudeEditTools.join('|'),
331+
hooks: [(input, toolID) => this._onWillEditTool(input, toolID, token)]
332+
});
333+
334+
if (!hooks.PostToolUse) {
335+
hooks.PostToolUse = [];
336+
}
337+
hooks.PostToolUse.push({
338+
matcher: claudeEditTools.join('|'),
339+
hooks: [(input, toolID) => this._onDidEditTool(input, toolID)]
340+
});
341+
342+
return hooks;
343+
}
344+
331345
private async _onWillEditTool(input: HookInput, toolUseID: string | undefined, token: CancellationToken): Promise<HookJSONOutput> {
332346
let uris: URI[] = [];
333347
try {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { HookCallbackMatcher, HookEvent } from '@anthropic-ai/claude-agent-sdk';
7+
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
8+
9+
/**
10+
* Constructor type for a hook handler class that implements HookCallbackMatcher.
11+
* The instantiation service will handle dependency injection.
12+
*/
13+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
14+
export type IClaudeHookHandlerCtor = new (...args: any[]) => HookCallbackMatcher;
15+
16+
/**
17+
* Registry mapping HookEvent types to their handler constructors.
18+
*/
19+
export type ClaudeHookRegistryType = Partial<Record<HookEvent, IClaudeHookHandlerCtor[]>>;
20+
21+
/**
22+
* Global registry of hook handler constructors organized by HookEvent.
23+
*/
24+
export const claudeHookRegistry: ClaudeHookRegistryType = {};
25+
26+
/**
27+
* Registers a hook handler constructor for a specific HookEvent.
28+
* Call this at module load time after defining a hook handler class.
29+
*
30+
* @param hookEvent The event type this handler responds to
31+
* @param ctor The constructor for the hook handler class
32+
*/
33+
export function registerClaudeHook(hookEvent: HookEvent, ctor: IClaudeHookHandlerCtor): void {
34+
if (!claudeHookRegistry[hookEvent]) {
35+
claudeHookRegistry[hookEvent] = [];
36+
}
37+
claudeHookRegistry[hookEvent]!.push(ctor);
38+
}
39+
40+
/**
41+
* Builds the hooks configuration object from the registry using dependency injection.
42+
*
43+
* @param instantiationService The instantiation service for creating hook instances with DI
44+
* @returns Hooks configuration object ready to pass to Claude SDK options
45+
*/
46+
export function buildHooksFromRegistry(
47+
instantiationService: IInstantiationService
48+
): Partial<Record<HookEvent, HookCallbackMatcher[]>> {
49+
const result: Partial<Record<HookEvent, HookCallbackMatcher[]>> = {};
50+
51+
for (const [hookEvent, ctors] of Object.entries(claudeHookRegistry) as [HookEvent, IClaudeHookHandlerCtor[]][]) {
52+
if (!ctors || ctors.length === 0) {
53+
continue;
54+
}
55+
56+
result[hookEvent] = ctors.map(ctor => instantiationService.createInstance(ctor));
57+
}
58+
59+
return result;
60+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
// Import all hook modules to trigger self-registration
7+
import './loggingHooks';
8+
import './sessionHooks';
9+
import './subagentHooks';
10+
import './toolHooks';
11+
12+
// Re-export registry and build function
13+
export { buildHooksFromRegistry, claudeHookRegistry, registerClaudeHook } from './claudeHookRegistry';
14+
export type { ClaudeHookRegistryType, IClaudeHookHandlerCtor } from './claudeHookRegistry';
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import {
7+
HookCallback,
8+
HookCallbackMatcher,
9+
HookInput,
10+
HookJSONOutput,
11+
NotificationHookInput,
12+
PermissionRequestHookInput,
13+
PreCompactHookInput,
14+
StopHookInput,
15+
UserPromptSubmitHookInput
16+
} from '@anthropic-ai/claude-agent-sdk';
17+
import { ILogService } from '../../../../../platform/log/common/logService';
18+
import { registerClaudeHook } from './claudeHookRegistry';
19+
20+
/**
21+
* Logging hook for Notification events.
22+
*/
23+
export class NotificationLoggingHook implements HookCallbackMatcher {
24+
public readonly hooks: HookCallback[];
25+
26+
constructor(
27+
@ILogService private readonly logService: ILogService
28+
) {
29+
this.hooks = [this._handle.bind(this)];
30+
}
31+
32+
private async _handle(input: HookInput): Promise<HookJSONOutput> {
33+
const hookInput = input as NotificationHookInput;
34+
this.logService.trace(`[ClaudeCodeSession] Notification Hook: title=${hookInput.title}, message=${hookInput.message}`);
35+
return { continue: true };
36+
}
37+
}
38+
registerClaudeHook('Notification', NotificationLoggingHook);
39+
40+
/**
41+
* Logging hook for UserPromptSubmit events.
42+
*/
43+
export class UserPromptSubmitLoggingHook implements HookCallbackMatcher {
44+
public readonly hooks: HookCallback[];
45+
46+
constructor(
47+
@ILogService private readonly logService: ILogService
48+
) {
49+
this.hooks = [this._handle.bind(this)];
50+
}
51+
52+
private async _handle(input: HookInput): Promise<HookJSONOutput> {
53+
const hookInput = input as UserPromptSubmitHookInput;
54+
this.logService.trace(`[ClaudeCodeSession] UserPromptSubmit Hook: prompt=${hookInput.prompt}`);
55+
return { continue: true };
56+
}
57+
}
58+
registerClaudeHook('UserPromptSubmit', UserPromptSubmitLoggingHook);
59+
60+
/**
61+
* Logging hook for Stop events.
62+
*/
63+
export class StopLoggingHook implements HookCallbackMatcher {
64+
public readonly hooks: HookCallback[];
65+
66+
constructor(
67+
@ILogService private readonly logService: ILogService
68+
) {
69+
this.hooks = [this._handle.bind(this)];
70+
}
71+
72+
private async _handle(input: HookInput): Promise<HookJSONOutput> {
73+
const hookInput = input as StopHookInput;
74+
this.logService.trace(`[ClaudeCodeSession] Stop Hook: stopHookActive=${hookInput.stop_hook_active}`);
75+
return { continue: true };
76+
}
77+
}
78+
registerClaudeHook('Stop', StopLoggingHook);
79+
80+
/**
81+
* Logging hook for PreCompact events.
82+
*/
83+
export class PreCompactLoggingHook implements HookCallbackMatcher {
84+
public readonly hooks: HookCallback[];
85+
86+
constructor(
87+
@ILogService private readonly logService: ILogService
88+
) {
89+
this.hooks = [this._handle.bind(this)];
90+
}
91+
92+
private async _handle(input: HookInput): Promise<HookJSONOutput> {
93+
const hookInput = input as PreCompactHookInput;
94+
this.logService.trace(`[ClaudeCodeSession] PreCompact Hook: trigger=${hookInput.trigger}, customInstructions=${hookInput.custom_instructions}`);
95+
return { continue: true };
96+
}
97+
}
98+
registerClaudeHook('PreCompact', PreCompactLoggingHook);
99+
100+
/**
101+
* Logging hook for PermissionRequest events.
102+
*/
103+
export class PermissionRequestLoggingHook implements HookCallbackMatcher {
104+
public readonly hooks: HookCallback[];
105+
106+
constructor(
107+
@ILogService private readonly logService: ILogService
108+
) {
109+
this.hooks = [this._handle.bind(this)];
110+
}
111+
112+
private async _handle(input: HookInput): Promise<HookJSONOutput> {
113+
const hookInput = input as PermissionRequestHookInput;
114+
this.logService.trace(`[ClaudeCodeSession] PermissionRequest Hook: tool=${hookInput.tool_name}, input=${JSON.stringify(hookInput.tool_input)}`);
115+
return { continue: true };
116+
}
117+
}
118+
registerClaudeHook('PermissionRequest', PermissionRequestLoggingHook);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import {
7+
HookCallback,
8+
HookCallbackMatcher,
9+
HookInput,
10+
HookJSONOutput,
11+
SessionEndHookInput,
12+
SessionStartHookInput
13+
} from '@anthropic-ai/claude-agent-sdk';
14+
import { ILogService } from '../../../../../platform/log/common/logService';
15+
import { registerClaudeHook } from './claudeHookRegistry';
16+
17+
/**
18+
* Logging hook for SessionStart events.
19+
*/
20+
export class SessionStartLoggingHook implements HookCallbackMatcher {
21+
public readonly hooks: HookCallback[];
22+
23+
constructor(
24+
@ILogService private readonly logService: ILogService
25+
) {
26+
this.hooks = [this._handle.bind(this)];
27+
}
28+
29+
private async _handle(input: HookInput): Promise<HookJSONOutput> {
30+
const hookInput = input as SessionStartHookInput;
31+
this.logService.trace(`[ClaudeCodeSession] SessionStart Hook: source=${hookInput.source}, sessionId=${hookInput.session_id}`);
32+
return { continue: true };
33+
}
34+
}
35+
registerClaudeHook('SessionStart', SessionStartLoggingHook);
36+
37+
/**
38+
* Logging hook for SessionEnd events.
39+
*/
40+
export class SessionEndLoggingHook implements HookCallbackMatcher {
41+
public readonly hooks: HookCallback[];
42+
43+
constructor(
44+
@ILogService private readonly logService: ILogService
45+
) {
46+
this.hooks = [this._handle.bind(this)];
47+
}
48+
49+
private async _handle(input: HookInput): Promise<HookJSONOutput> {
50+
const hookInput = input as SessionEndHookInput;
51+
this.logService.trace(`[ClaudeCodeSession] SessionEnd Hook: reason=${hookInput.reason}, sessionId=${hookInput.session_id}`);
52+
return { continue: true };
53+
}
54+
}
55+
registerClaudeHook('SessionEnd', SessionEndLoggingHook);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import {
7+
HookCallback,
8+
HookCallbackMatcher,
9+
HookInput,
10+
HookJSONOutput,
11+
SubagentStartHookInput,
12+
SubagentStopHookInput
13+
} from '@anthropic-ai/claude-agent-sdk';
14+
import { ILogService } from '../../../../../platform/log/common/logService';
15+
import { registerClaudeHook } from './claudeHookRegistry';
16+
17+
/**
18+
* Logging hook for SubagentStart events.
19+
*/
20+
export class SubagentStartLoggingHook implements HookCallbackMatcher {
21+
public readonly hooks: HookCallback[];
22+
23+
constructor(
24+
@ILogService private readonly logService: ILogService
25+
) {
26+
this.hooks = [this._handle.bind(this)];
27+
}
28+
29+
private async _handle(input: HookInput): Promise<HookJSONOutput> {
30+
const hookInput = input as SubagentStartHookInput;
31+
this.logService.trace(`[ClaudeCodeSession] SubagentStart Hook: agentId=${hookInput.agent_id}, agentType=${hookInput.agent_type}`);
32+
return { continue: true };
33+
}
34+
}
35+
registerClaudeHook('SubagentStart', SubagentStartLoggingHook);
36+
37+
/**
38+
* Logging hook for SubagentStop events.
39+
*/
40+
export class SubagentStopLoggingHook implements HookCallbackMatcher {
41+
public readonly hooks: HookCallback[];
42+
43+
constructor(
44+
@ILogService private readonly logService: ILogService
45+
) {
46+
this.hooks = [this._handle.bind(this)];
47+
}
48+
49+
private async _handle(input: HookInput): Promise<HookJSONOutput> {
50+
const hookInput = input as SubagentStopHookInput;
51+
this.logService.trace(`[ClaudeCodeSession] SubagentStop Hook: stopHookActive=${hookInput.stop_hook_active}`);
52+
return { continue: true };
53+
}
54+
}
55+
registerClaudeHook('SubagentStop', SubagentStopLoggingHook);

0 commit comments

Comments
 (0)