Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts
5243 views
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 { URI } from '../../../../../base/common/uri.js';
7
import { HookType, IHookCommand, toHookType, resolveHookCommand } from './hookSchema.js';
8
9
/**
10
* Maps Claude hook type names to our abstract HookType.
11
* Claude uses PascalCase and slightly different names.
12
* @see https://docs.anthropic.com/en/docs/claude-code/hooks
13
*/
14
export const CLAUDE_HOOK_TYPE_MAP: Record<string, HookType> = {
15
'SessionStart': HookType.SessionStart,
16
'UserPromptSubmit': HookType.UserPromptSubmit,
17
'PreToolUse': HookType.PreToolUse,
18
'PostToolUse': HookType.PostToolUse,
19
'PreCompact': HookType.PreCompact,
20
'SubagentStart': HookType.SubagentStart,
21
'SubagentStop': HookType.SubagentStop,
22
'Stop': HookType.Stop,
23
};
24
25
/**
26
* Cached inverse mapping from HookType to Claude hook type name.
27
* Lazily computed on first access.
28
*/
29
let _hookTypeToClaudeName: Map<HookType, string> | undefined;
30
31
function getHookTypeToClaudeNameMap(): Map<HookType, string> {
32
if (!_hookTypeToClaudeName) {
33
_hookTypeToClaudeName = new Map();
34
for (const [claudeName, hookType] of Object.entries(CLAUDE_HOOK_TYPE_MAP)) {
35
_hookTypeToClaudeName.set(hookType, claudeName);
36
}
37
}
38
return _hookTypeToClaudeName;
39
}
40
41
/**
42
* Resolves a Claude hook type name to our abstract HookType.
43
*/
44
export function resolveClaudeHookType(name: string): HookType | undefined {
45
return CLAUDE_HOOK_TYPE_MAP[name];
46
}
47
48
/**
49
* Gets the Claude hook type name for a given abstract HookType.
50
* Returns undefined if the hook type is not supported in Claude.
51
*/
52
export function getClaudeHookTypeName(hookType: HookType): string | undefined {
53
return getHookTypeToClaudeNameMap().get(hookType);
54
}
55
56
/**
57
* Parses hooks from a Claude settings.json file.
58
* Claude format:
59
* {
60
* "hooks": {
61
* "PreToolUse": [
62
* { "matcher": "Bash", "hooks": [{ "type": "command", "command": "..." }] }
63
* ]
64
* }
65
* }
66
*
67
* Or simpler format:
68
* {
69
* "hooks": {
70
* "PreToolUse": [{ "type": "command", "command": "..." }]
71
* }
72
* }
73
*/
74
export function parseClaudeHooks(
75
json: unknown,
76
workspaceRootUri: URI | undefined,
77
userHome: string
78
): Map<HookType, { hooks: IHookCommand[]; originalId: string }> {
79
const result = new Map<HookType, { hooks: IHookCommand[]; originalId: string }>();
80
81
if (!json || typeof json !== 'object') {
82
return result;
83
}
84
85
const root = json as Record<string, unknown>;
86
const hooks = root.hooks;
87
88
if (!hooks || typeof hooks !== 'object') {
89
return result;
90
}
91
92
const hooksObj = hooks as Record<string, unknown>;
93
94
for (const originalId of Object.keys(hooksObj)) {
95
// Resolve Claude hook type name to our canonical HookType
96
const hookType = resolveClaudeHookType(originalId) ?? toHookType(originalId);
97
if (!hookType) {
98
continue;
99
}
100
101
const hookArray = hooksObj[originalId];
102
if (!Array.isArray(hookArray)) {
103
continue;
104
}
105
106
const commands: IHookCommand[] = [];
107
108
for (const item of hookArray) {
109
if (!item || typeof item !== 'object') {
110
continue;
111
}
112
113
const itemObj = item as Record<string, unknown>;
114
115
// Claude can have nested hooks with matchers: { matcher: "Bash", hooks: [...] }
116
const nestedHooks = (itemObj as { hooks?: unknown }).hooks;
117
if (nestedHooks !== undefined && Array.isArray(nestedHooks)) {
118
for (const nestedHook of nestedHooks) {
119
const resolved = resolveClaudeCommand(nestedHook as Record<string, unknown>, workspaceRootUri, userHome);
120
if (resolved) {
121
commands.push(resolved);
122
}
123
}
124
} else {
125
// Direct hook command
126
const resolved = resolveClaudeCommand(itemObj, workspaceRootUri, userHome);
127
if (resolved) {
128
commands.push(resolved);
129
}
130
}
131
}
132
133
if (commands.length > 0) {
134
const existing = result.get(hookType);
135
if (existing) {
136
existing.hooks.push(...commands);
137
} else {
138
result.set(hookType, { hooks: commands, originalId });
139
}
140
}
141
}
142
143
return result;
144
}
145
146
/**
147
* Resolves a Claude hook command to our IHookCommand format.
148
* Claude commands can be: { type: "command", command: "..." } or { command: "..." }
149
*/
150
function resolveClaudeCommand(
151
raw: Record<string, unknown>,
152
workspaceRootUri: URI | undefined,
153
userHome: string
154
): IHookCommand | undefined {
155
// Claude might not require 'type' field, so we're more lenient
156
const hasValidType = raw.type === undefined || raw.type === 'command';
157
if (!hasValidType) {
158
return undefined;
159
}
160
161
// Add type if missing for resolveHookCommand
162
const normalized = { ...raw, type: 'command' };
163
return resolveHookCommand(normalized, workspaceRootUri, userHome);
164
}
165
166