Path: blob/main/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts
5243 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { URI } from '../../../../../base/common/uri.js';6import { HookType, IHookCommand, toHookType, resolveHookCommand } from './hookSchema.js';78/**9* Maps Claude hook type names to our abstract HookType.10* Claude uses PascalCase and slightly different names.11* @see https://docs.anthropic.com/en/docs/claude-code/hooks12*/13export const CLAUDE_HOOK_TYPE_MAP: Record<string, HookType> = {14'SessionStart': HookType.SessionStart,15'UserPromptSubmit': HookType.UserPromptSubmit,16'PreToolUse': HookType.PreToolUse,17'PostToolUse': HookType.PostToolUse,18'PreCompact': HookType.PreCompact,19'SubagentStart': HookType.SubagentStart,20'SubagentStop': HookType.SubagentStop,21'Stop': HookType.Stop,22};2324/**25* Cached inverse mapping from HookType to Claude hook type name.26* Lazily computed on first access.27*/28let _hookTypeToClaudeName: Map<HookType, string> | undefined;2930function getHookTypeToClaudeNameMap(): Map<HookType, string> {31if (!_hookTypeToClaudeName) {32_hookTypeToClaudeName = new Map();33for (const [claudeName, hookType] of Object.entries(CLAUDE_HOOK_TYPE_MAP)) {34_hookTypeToClaudeName.set(hookType, claudeName);35}36}37return _hookTypeToClaudeName;38}3940/**41* Resolves a Claude hook type name to our abstract HookType.42*/43export function resolveClaudeHookType(name: string): HookType | undefined {44return CLAUDE_HOOK_TYPE_MAP[name];45}4647/**48* Gets the Claude hook type name for a given abstract HookType.49* Returns undefined if the hook type is not supported in Claude.50*/51export function getClaudeHookTypeName(hookType: HookType): string | undefined {52return getHookTypeToClaudeNameMap().get(hookType);53}5455/**56* Parses hooks from a Claude settings.json file.57* Claude format:58* {59* "hooks": {60* "PreToolUse": [61* { "matcher": "Bash", "hooks": [{ "type": "command", "command": "..." }] }62* ]63* }64* }65*66* Or simpler format:67* {68* "hooks": {69* "PreToolUse": [{ "type": "command", "command": "..." }]70* }71* }72*/73export function parseClaudeHooks(74json: unknown,75workspaceRootUri: URI | undefined,76userHome: string77): Map<HookType, { hooks: IHookCommand[]; originalId: string }> {78const result = new Map<HookType, { hooks: IHookCommand[]; originalId: string }>();7980if (!json || typeof json !== 'object') {81return result;82}8384const root = json as Record<string, unknown>;85const hooks = root.hooks;8687if (!hooks || typeof hooks !== 'object') {88return result;89}9091const hooksObj = hooks as Record<string, unknown>;9293for (const originalId of Object.keys(hooksObj)) {94// Resolve Claude hook type name to our canonical HookType95const hookType = resolveClaudeHookType(originalId) ?? toHookType(originalId);96if (!hookType) {97continue;98}99100const hookArray = hooksObj[originalId];101if (!Array.isArray(hookArray)) {102continue;103}104105const commands: IHookCommand[] = [];106107for (const item of hookArray) {108if (!item || typeof item !== 'object') {109continue;110}111112const itemObj = item as Record<string, unknown>;113114// Claude can have nested hooks with matchers: { matcher: "Bash", hooks: [...] }115const nestedHooks = (itemObj as { hooks?: unknown }).hooks;116if (nestedHooks !== undefined && Array.isArray(nestedHooks)) {117for (const nestedHook of nestedHooks) {118const resolved = resolveClaudeCommand(nestedHook as Record<string, unknown>, workspaceRootUri, userHome);119if (resolved) {120commands.push(resolved);121}122}123} else {124// Direct hook command125const resolved = resolveClaudeCommand(itemObj, workspaceRootUri, userHome);126if (resolved) {127commands.push(resolved);128}129}130}131132if (commands.length > 0) {133const existing = result.get(hookType);134if (existing) {135existing.hooks.push(...commands);136} else {137result.set(hookType, { hooks: commands, originalId });138}139}140}141142return result;143}144145/**146* Resolves a Claude hook command to our IHookCommand format.147* Claude commands can be: { type: "command", command: "..." } or { command: "..." }148*/149function resolveClaudeCommand(150raw: Record<string, unknown>,151workspaceRootUri: URI | undefined,152userHome: string153): IHookCommand | undefined {154// Claude might not require 'type' field, so we're more lenient155const hasValidType = raw.type === undefined || raw.type === 'command';156if (!hasValidType) {157return undefined;158}159160// Add type if missing for resolveHookCommand161const normalized = { ...raw, type: 'command' };162return resolveHookCommand(normalized, workspaceRootUri, userHome);163}164165166