Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit abb8ee3

Browse files
committed
feat: add hooks settings UI and service infrastructure
Add comprehensive hooks system with: - HooksService for managing hook configurations - hooks-file-parser for parsing hook definition files - Complete HooksSettings UI components including: - CreateHookDialog, DeleteHookDialog - HookActionEditor, HookCommandTab, HookConfigTab - HookEventGroup, HookItem, HookLogsTab - HookSessionMatchers, HookToolMatchers - Custom hooks (useDebouncedCallback, useHookDragDrop, etc.) - TypeScript types for hooks in packages/types - Integration with ClineProvider and webviewMessageHandler - Updated settings.json translations
1 parent 0cd257a commit abb8ee3

29 files changed

Lines changed: 4899 additions & 8 deletions

packages/types/src/hooks.ts

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import { z } from "zod"
2+
3+
/**
4+
* Hook Event Types
5+
*
6+
* All supported hook event types that can trigger hook execution.
7+
* Based on Claude Code's hook system.
8+
*/
9+
export const hookEventTypes = [
10+
"SessionStart",
11+
"UserPromptSubmit",
12+
"PreToolUse",
13+
"PermissionRequest",
14+
"PostToolUse",
15+
"PostToolUseFailure",
16+
"SubtaskStart",
17+
"SubtaskStop",
18+
"Stop",
19+
"PreCompact",
20+
"SessionEnd",
21+
] as const
22+
23+
export const hookEventTypeSchema = z.enum(hookEventTypes)
24+
export type HookEventType = z.infer<typeof hookEventTypeSchema>
25+
26+
/**
27+
* Tool Matchers
28+
*
29+
* Categories of tools that can be matched for tool-related hooks.
30+
* Used with PreToolUse, PostToolUse, PostToolUseFailure, and PermissionRequest events.
31+
*/
32+
export const toolMatchers = ["read", "edit", "browser", "command", "mcp", "modes"] as const
33+
export const toolMatcherSchema = z.enum(toolMatchers)
34+
export type ToolMatcher = z.infer<typeof toolMatcherSchema>
35+
36+
/**
37+
* Session Matchers
38+
*
39+
* Session state types that can be matched for SessionStart hooks.
40+
*/
41+
export const sessionMatchers = ["startup", "resume", "clear", "compact"] as const
42+
export const sessionMatcherSchema = z.enum(sessionMatchers)
43+
export type SessionMatcher = z.infer<typeof sessionMatcherSchema>
44+
45+
/**
46+
* Action Types
47+
*
48+
* Types of actions that hooks can execute.
49+
*/
50+
export const hookActionTypes = ["command", "slashCommand"] as const
51+
export const hookActionTypeSchema = z.enum(hookActionTypes)
52+
export type HookActionType = z.infer<typeof hookActionTypeSchema>
53+
54+
/**
55+
* Shell Command Action
56+
*
57+
* Executes a shell command with optional working directory and timeout.
58+
*/
59+
export const shellCommandActionSchema = z.object({
60+
type: z.literal("command"),
61+
command: z.string().min(1),
62+
cwd: z.string().optional(),
63+
timeout: z.number().min(1).max(300).default(30),
64+
})
65+
66+
export type ShellCommandAction = z.infer<typeof shellCommandActionSchema>
67+
68+
/**
69+
* Slash Command Action
70+
*
71+
* Executes an existing slash command defined in the project or globally.
72+
*/
73+
export const slashCommandActionSchema = z.object({
74+
type: z.literal("slashCommand"),
75+
command: z.string().min(1).startsWith("/"),
76+
args: z.string().optional(),
77+
})
78+
79+
export type SlashCommandAction = z.infer<typeof slashCommandActionSchema>
80+
81+
/**
82+
* Hook Action
83+
*
84+
* Union of all action types that hooks can execute.
85+
*/
86+
export const hookActionSchema = z.discriminatedUnion("type", [shellCommandActionSchema, slashCommandActionSchema])
87+
88+
export type HookAction = z.infer<typeof hookActionSchema>
89+
90+
/**
91+
* Tool Matchers Config
92+
*
93+
* Configuration for matching tool-related hooks.
94+
* Includes both category matchers and custom regex patterns.
95+
*/
96+
export const toolMatchersConfigSchema = z.object({
97+
tools: z.array(toolMatcherSchema).optional(),
98+
customPattern: z.string().nullable().optional(),
99+
})
100+
101+
export type ToolMatchersConfig = z.infer<typeof toolMatchersConfigSchema>
102+
103+
/**
104+
* Session Matchers Config
105+
*
106+
* Configuration for matching SessionStart hooks.
107+
*/
108+
export const sessionMatchersConfigSchema = z.object({
109+
sessionType: z.array(sessionMatcherSchema).optional(),
110+
})
111+
112+
export type SessionMatchersConfig = z.infer<typeof sessionMatchersConfigSchema>
113+
114+
/**
115+
* Hook Configuration
116+
*
117+
* Individual hook configuration including ID, name, enabled state, action, and matchers.
118+
*/
119+
export const hookConfigSchema = z.object({
120+
id: z
121+
.string()
122+
.min(1)
123+
.regex(/^[a-z0-9-]+$/),
124+
name: z.string().min(1),
125+
enabled: z.boolean().default(true),
126+
action: hookActionSchema,
127+
matchers: z.union([toolMatchersConfigSchema, sessionMatchersConfigSchema]).optional(),
128+
})
129+
130+
export type HookConfig = z.infer<typeof hookConfigSchema>
131+
132+
/**
133+
* Hooks File Structure
134+
*
135+
* Full structure of a .hooks configuration file.
136+
*/
137+
export const hooksFileSchema = z.object({
138+
$schema: z.string().optional(),
139+
version: z.literal("1.0"),
140+
hooks: z.record(hookEventTypeSchema, z.array(hookConfigSchema)).default({}),
141+
})
142+
143+
export type HooksFile = z.infer<typeof hooksFileSchema>
144+
145+
/**
146+
* Hook with Metadata
147+
*
148+
* Extended hook configuration with additional metadata for UI display.
149+
*/
150+
export interface HookWithMetadata extends HookConfig {
151+
eventType: HookEventType
152+
source: "global" | "project"
153+
filePath?: string
154+
}
155+
156+
/**
157+
* Hook Event Descriptions
158+
*
159+
* Human-readable descriptions for each hook event type.
160+
*/
161+
export const hookEventDescriptions: Record<HookEventType, string> = {
162+
SessionStart: "Session begins or resumes",
163+
UserPromptSubmit: "User submits a prompt",
164+
PreToolUse: "Before tool execution",
165+
PermissionRequest: "When permission dialog appears",
166+
PostToolUse: "After tool succeeds",
167+
PostToolUseFailure: "After tool fails",
168+
SubtaskStart: "When spawning a subtask",
169+
SubtaskStop: "When subtask finishes",
170+
Stop: "Claude finishes responding",
171+
PreCompact: "Before context compaction",
172+
SessionEnd: "Session terminates",
173+
}
174+
175+
/**
176+
* Tool Matcher Descriptions
177+
*
178+
* Human-readable descriptions for each tool matcher category.
179+
*/
180+
export const toolMatcherDescriptions: Record<ToolMatcher, string> = {
181+
read: "file reading",
182+
edit: "file writing",
183+
browser: "web tools",
184+
command: "shell/bash",
185+
mcp: "protocol tools",
186+
modes: "mode tools",
187+
}
188+
189+
/**
190+
* Session Matcher Descriptions
191+
*
192+
* Human-readable descriptions for each session matcher type.
193+
*/
194+
export const sessionMatcherDescriptions: Record<SessionMatcher, string> = {
195+
startup: "new session",
196+
resume: "existing session",
197+
clear: "conversation cleared",
198+
compact: "context compacted",
199+
}
200+
201+
/**
202+
* Helper to determine if a hook event type uses tool matchers.
203+
*
204+
* @param eventType - The hook event type to check
205+
* @returns true if the event type supports tool matchers
206+
*/
207+
export function usesToolMatchers(eventType: HookEventType): boolean {
208+
return ["PreToolUse", "PostToolUse", "PostToolUseFailure", "PermissionRequest"].includes(eventType)
209+
}
210+
211+
/**
212+
* Helper to determine if a hook event type uses session matchers.
213+
*
214+
* @param eventType - The hook event type to check
215+
* @returns true if the event type supports session matchers
216+
*/
217+
export function usesSessionMatchers(eventType: HookEventType): boolean {
218+
return eventType === "SessionStart"
219+
}
220+
221+
/**
222+
* Helper to determine if a hook event type uses no matchers.
223+
*
224+
* These hooks fire unconditionally when their event occurs.
225+
*
226+
* @param eventType - The hook event type to check
227+
* @returns true if the event type has no matchers
228+
*/
229+
export function usesNoMatchers(eventType: HookEventType): boolean {
230+
return !usesToolMatchers(eventType) && !usesSessionMatchers(eventType)
231+
}
232+
233+
/**
234+
* Tool ID to Matcher Mapping
235+
*
236+
* Maps internal tool IDs to their corresponding matcher categories.
237+
* Used to determine which hooks should fire for a given tool.
238+
*/
239+
export const TOOL_TO_MATCHER: Record<string, ToolMatcher> = {
240+
// Read
241+
read_file: "read",
242+
list_files: "read",
243+
search_files: "read",
244+
codebase_search: "read",
245+
fetch_instructions: "read",
246+
247+
// Edit
248+
write_to_file: "edit",
249+
apply_diff: "edit",
250+
apply_patch: "edit",
251+
edit_file: "edit",
252+
search_and_replace: "edit",
253+
search_replace: "edit",
254+
255+
// Browser
256+
browser_action: "browser",
257+
258+
// Command
259+
execute_command: "command",
260+
261+
// MCP
262+
use_mcp_tool: "mcp",
263+
access_mcp_resource: "mcp",
264+
265+
// Modes
266+
switch_mode: "modes",
267+
new_task: "modes",
268+
}

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from "./followup.js"
1111
export * from "./git.js"
1212
export * from "./global-settings.js"
1313
export * from "./history.js"
14+
export * from "./hooks.js"
1415
export * from "./image-generation.js"
1516
export * from "./ipc.js"
1617
export * from "./marketplace.js"

packages/types/src/vscode-extension-host.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type { SkillMetadata } from "./skills.js"
2222
import type { ModelRecord, RouterModels } from "./model.js"
2323
import type { OpenAiCodexRateLimitInfo } from "./providers/openai-codex-rate-limits.js"
2424
import type { WorktreeIncludeStatus } from "./worktree.js"
25+
import type { HookConfig, HookEventType, HookWithMetadata } from "./hooks.js"
2526

2627
/**
2728
* ExtensionMessage
@@ -109,6 +110,9 @@ export interface ExtensionMessage {
109110
| "branchWorktreeIncludeResult"
110111
| "folderSelected"
111112
| "skills"
113+
// Hooks messages
114+
| "hooks/loaded"
115+
| "hooks/error"
112116
text?: string
113117
payload?: any // eslint-disable-line @typescript-eslint/no-explicit-any
114118
checkpointWarning?: {
@@ -211,6 +215,8 @@ export interface ExtensionMessage {
211215
childrenCost: number
212216
}
213217
historyItem?: HistoryItem
218+
// Hooks message payloads
219+
hooks?: HookWithMetadata[]
214220
taskHistory?: HistoryItem[] // For taskHistoryUpdated: full sorted task history
215221
/** For taskHistoryItemUpdated: single updated/added history item */
216222
taskHistoryItem?: HistoryItem
@@ -605,6 +611,14 @@ export interface WebviewMessage {
605611
| "createSkill"
606612
| "deleteSkill"
607613
| "openSkillFile"
614+
// Hooks messages
615+
| "hooks/load"
616+
| "hooks/save"
617+
| "hooks/delete"
618+
| "hooks/reorder"
619+
| "hooks/move"
620+
| "hooks/openFolder"
621+
| "hooks/reload"
608622
text?: string
609623
editedMessageContent?: string
610624
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
@@ -697,6 +711,13 @@ export interface WebviewMessage {
697711
codebaseIndexOpenRouterApiKey?: string
698712
}
699713
updatedSettings?: RooCodeSettings
714+
// Hooks message payloads
715+
hook?: HookConfig
716+
hookId?: string
717+
eventType?: HookEventType
718+
fromEventType?: HookEventType
719+
toEventType?: HookEventType
720+
hookIds?: string[]
700721
// Worktree properties
701722
worktreePath?: string
702723
worktreeBranch?: string

0 commit comments

Comments
 (0)