Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
22eda99
feat: implement Claude Code-style hooks system
taltas Jan 16, 2026
85f2bd6
fix: clarify tool names for hooks matchers
taltas Jan 16, 2026
e079e6c
feat: improve hooks settings UI
taltas Jan 16, 2026
ff4d057
feat: enhance hooks UI with delete support and status indicators
taltas Jan 17, 2026
a13fba4
feat: open hook config file from UI
taltas Jan 17, 2026
8bb5b61
fix: improve hooks settings layout and UI
taltas Jan 17, 2026
08b41f2
feat: display hook execution in chat
taltas Jan 17, 2026
6ebe2da
feat: gate hooks behind experimental flag
taltas Jan 17, 2026
ad1cbc2
fix: make initializeHookManager public to resolve TS error
taltas Jan 17, 2026
f8bd3ba
feat: enable continuous hooks configuration reload
taltas Jan 17, 2026
36ca0cc
feat: add Create New Hook button and fix UI styling
taltas Jan 17, 2026
5f1237f
fix: enforce master toggle for hooks execution
taltas Jan 17, 2026
fe8f6ce
feat: display hook ID in activity log
taltas Jan 17, 2026
cd3ef8e
chore: add translations for hooks settings
taltas Jan 17, 2026
2cac563
style: update hooks icon to FishingHook
taltas Jan 17, 2026
fe0e61f
feat: promote hooks from experimental to permanent feature
taltas Jan 17, 2026
0f00dc7
feat(hooks): add tool group matchers for simplified hook configuration
taltas Jan 17, 2026
ad3cd20
Fix: Replace old verbose matcher syntax with tool group in example hook
taltas Jan 17, 2026
609ff88
feat(hooks): enhance configuration UI with event checkboxes and tool …
taltas Jan 18, 2026
469d7fe
fix(hooks): align tests and handler typing for update flow
taltas Jan 18, 2026
d9509c6
feat(hooks): restructure yaml schema and enhance ui configuration (ph…
taltas Jan 18, 2026
7d0d714
fix: preserve multi-event hooks and multi-group matchers
taltas Jan 18, 2026
caa9fd5
fix: dedupe multi-event hooks in settings UI
taltas Jan 18, 2026
6e443bf
feat: show hook execution output in chat
taltas Jan 18, 2026
64c4fcb
test: fix types test import extension
taltas Jan 18, 2026
05a7b3f
These should not have been in the branch in the first place
taltas Jan 18, 2026
c0d8507
i18n: add translations for hook triggered message
taltas Jan 18, 2026
8ed81bc
feat: add event-specific matchers for hooks
taltas Jan 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { hookExecutionOutputStatusSchema } from "../vscode-extension-host.js"

describe("hookExecutionOutputStatusSchema", () => {
it("accepts a valid started payload", () => {
const result = hookExecutionOutputStatusSchema.safeParse({
executionId: "exec_1",
hookId: "hook_1",
event: "PreToolUse",
status: "started",
command: "echo hi",
cwd: "/project",
})

expect(result.success).toBe(true)
})

it("requires output for status=output", () => {
const result = hookExecutionOutputStatusSchema.safeParse({
executionId: "exec_1",
hookId: "hook_1",
event: "PreToolUse",
status: "output",
command: "echo hi",
cwd: "/project",
// output missing
})

expect(result.success).toBe(false)
})

it("accepts a valid exited payload", () => {
const result = hookExecutionOutputStatusSchema.safeParse({
executionId: "exec_1",
hookId: "hook_1",
event: "PreToolUse",
status: "exited",
command: "echo hi",
cwd: "/project",
exitCode: 0,
durationMs: 123,
})

expect(result.success).toBe(true)
})

it("rejects unknown status", () => {
const result = hookExecutionOutputStatusSchema.safeParse({
executionId: "exec_1",
hookId: "hook_1",
event: "PreToolUse",
status: "unknown",
command: "echo hi",
cwd: "/project",
})

expect(result.success).toBe(false)
})
})
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export const globalSettingsSchema = z.object({

mcpEnabled: z.boolean().optional(),
enableMcpServerCreation: z.boolean().optional(),
hooksEnabled: z.boolean().optional(),

mode: z.string().optional(),
modeApiConfigs: z.record(z.string(), z.string()).optional(),
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
* - `condense_context`: Context condensation/summarization has started
* - `condense_context_error`: Error occurred during context condensation
* - `codebase_search_result`: Results from searching the codebase
* - `hook_triggered`: Notification that a hook has been executed
*/
export const clineSays = [
"error",
Expand Down Expand Up @@ -179,6 +180,8 @@ export const clineSays = [
"condense_context_error",
"sliding_window_truncation",
"codebase_search_result",
"hook_execution",
"hook_triggered",
"user_edit_todos",
] as const

Expand Down
250 changes: 250 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface ExtensionMessage {
| "acceptInput"
| "setHistoryPreviewCollapsed"
| "commandExecutionStatus"
| "hookExecutionOutputStatus"
| "mcpExecutionStatus"
| "vsCodeSetting"
| "authenticatedUser"
Expand Down Expand Up @@ -95,6 +96,8 @@ export interface ExtensionMessage {
| "customToolsResult"
| "modes"
| "taskWithAggregatedCosts"
| "hookExecutionStatus"
| "hooksCopyHookResult"
text?: string
payload?: any // eslint-disable-line @typescript-eslint/no-explicit-any
checkpointWarning?: {
Expand Down Expand Up @@ -190,6 +193,224 @@ export interface ExtensionMessage {
childrenCost: number
}
historyItem?: HistoryItem
hookExecutionStatus?: HookExecutionStatusPayload
}

/**
* HookExecutionOutputStatusPayload
*
* Streaming terminal-style hook execution status updates.
*
* Pattern matches `commandExecutionStatus`: the payload is serialized JSON in
* [`ExtensionMessage.text`](packages/types/src/vscode-extension-host.ts:100).
*/
export const hookExecutionOutputStatusSchema = z.discriminatedUnion("status", [
z.object({
executionId: z.string(),
hookId: z.string(),
event: z.string(),
toolName: z.string().optional(),
status: z.literal("started"),
command: z.string(),
cwd: z.string(),
shell: z.string().optional(),
pid: z.number().optional(),
output: z.string().optional(),
exitCode: z.number().optional(),
durationMs: z.number().optional(),
blockMessage: z.string().optional(),
error: z.string().optional(),
modified: z.boolean().optional(),
}),
z.object({
executionId: z.string(),
hookId: z.string(),
event: z.string(),
toolName: z.string().optional(),
status: z.literal("output"),
command: z.string(),
cwd: z.string(),
shell: z.string().optional(),
pid: z.number().optional(),
output: z.string(),
exitCode: z.number().optional(),
durationMs: z.number().optional(),
blockMessage: z.string().optional(),
error: z.string().optional(),
modified: z.boolean().optional(),
}),
z.object({
executionId: z.string(),
hookId: z.string(),
event: z.string(),
toolName: z.string().optional(),
status: z.literal("exited"),
command: z.string(),
cwd: z.string(),
shell: z.string().optional(),
pid: z.number().optional(),
output: z.string().optional(),
exitCode: z.number().optional(),
durationMs: z.number().optional(),
blockMessage: z.string().optional(),
error: z.string().optional(),
modified: z.boolean().optional(),
}),
z.object({
executionId: z.string(),
hookId: z.string(),
event: z.string(),
toolName: z.string().optional(),
status: z.literal("blocked"),
command: z.string(),
cwd: z.string(),
shell: z.string().optional(),
pid: z.number().optional(),
output: z.string().optional(),
exitCode: z.number().optional(),
durationMs: z.number().optional(),
blockMessage: z.string().optional(),
error: z.string().optional(),
modified: z.boolean().optional(),
}),
z.object({
executionId: z.string(),
hookId: z.string(),
event: z.string(),
toolName: z.string().optional(),
status: z.literal("failed"),
command: z.string(),
cwd: z.string(),
shell: z.string().optional(),
pid: z.number().optional(),
output: z.string().optional(),
exitCode: z.number().optional(),
durationMs: z.number().optional(),
blockMessage: z.string().optional(),
error: z.string().optional(),
modified: z.boolean().optional(),
}),
z.object({
executionId: z.string(),
hookId: z.string(),
event: z.string(),
toolName: z.string().optional(),
status: z.literal("fallback"),
command: z.string(),
cwd: z.string(),
shell: z.string().optional(),
pid: z.number().optional(),
output: z.string().optional(),
exitCode: z.number().optional(),
durationMs: z.number().optional(),
blockMessage: z.string().optional(),
error: z.string().optional(),
modified: z.boolean().optional(),
}),
])

export type HookExecutionOutputStatusPayload = z.infer<typeof hookExecutionOutputStatusSchema>

/**
* HookExecutionStatusPayload
* Sent when hook execution starts, completes, or fails.
*/
export interface HookExecutionStatusPayload {
/** Status of the hook execution */
status: "running" | "completed" | "failed" | "blocked"
/** Event type that triggered the hook */
event: string
/** Tool name if this is a tool-related event */
toolName?: string
/** Hook ID being executed */
hookId?: string
/** Duration in milliseconds (only for completed/failed) */
duration?: number
/** Error message if failed */
error?: string
/** Block message if hook blocked the operation */
blockMessage?: string
/** Whether tool input was modified */
modified?: boolean
}

/**
* Serializable hook information for webview display.
* This is a subset of ResolvedHook that can be safely serialized to JSON.
*/
export interface HookInfo {
/** Unique identifier for this hook */
id: string
/** File path where this hook was defined (if known) */
filePath?: string
/** The event type this hook is registered for */
event: string
/**
* All event types this hook ID is registered for.
*
* Backwards-compatible: older extensions may only provide `event`.
*/
events?: string[]
/** Tool name filter (regex/glob pattern) */
matcher?: string
/** Preview of the command (truncated for display) */
commandPreview: string
/** Whether this hook is enabled */
enabled: boolean
/** Source of this hook configuration */
source: "project" | "mode" | "global"
/**
* File creation timestamp (ms since epoch) for the config file this hook came from.
* Used for stable UI sorting.
*/
createdAt?: number
/** Timeout in seconds */
timeout: number
/** Override shell if specified */
shell?: string
/** Human-readable description */
description?: string
}

/**
* Serializable hook execution record for webview display.
*/
export interface HookExecutionRecord {
/** When the hook was executed (ISO string) */
timestamp: string
/** The hook ID that was executed */
hookId: string
/** The event that triggered execution */
event: string
/** Tool name if this was a tool-related event */
toolName?: string
/** Exit code from the process */
exitCode: number | null
/** Execution duration in milliseconds */
duration: number
/** Whether the hook timed out */
timedOut: boolean
/** Whether the hook blocked execution */
blocked: boolean
/** Error message if the hook failed */
error?: string
/** Block message if the hook blocked */
blockMessage?: string
}

/**
* Hooks state for webview display.
* Contains all information needed to render the Hooks settings tab.
*/
export interface HooksState {
/** Array of resolved hooks with display information */
enabledHooks: HookInfo[]
/** Recent execution history (last N records) */
executionHistory: HookExecutionRecord[]
/** Whether project-level hooks are present (for security warnings) */
hasProjectHooks: boolean
/** When the config snapshot was last loaded (ISO string) */
snapshotTimestamp?: string
}

export type ExtensionState = Pick<
Expand Down Expand Up @@ -289,6 +510,7 @@ export type ExtensionState = Pick<

mcpEnabled: boolean
enableMcpServerCreation: boolean
hooksEnabled: boolean

mode: string
customModes: ModeConfig[]
Expand Down Expand Up @@ -335,6 +557,9 @@ export type ExtensionState = Pick<
claudeCodeIsAuthenticated?: boolean
openAiCodexIsAuthenticated?: boolean
debug?: boolean

/** Hooks configuration and execution state for the Hooks settings tab */
hooks?: HooksState
}

export interface Command {
Expand Down Expand Up @@ -521,6 +746,15 @@ export interface WebviewMessage {
| "requestModes"
| "switchMode"
| "debugSetting"
| "hooksReloadConfig"
| "hooksSetEnabled"
| "hooksSetAllEnabled"
| "hooksOpenConfigFolder"
| "hooksDeleteHook"
| "hooksOpenHookFile"
| "hooksCreateNew"
| "hooksUpdateHook"
| "hooksCopyHook"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
Expand Down Expand Up @@ -576,6 +810,22 @@ export interface WebviewMessage {
list?: string[] // For dismissedUpsells response
organizationId?: string | null // For organization switching
useProviderSignup?: boolean // For rooCloudSignIn to use provider signup flow
hookId?: string // For hooksSetEnabled, hooksDeleteHook
hookEnabled?: boolean // For hooksSetEnabled
hooksEnabled?: boolean // For hooksSetAllEnabled
hooksSource?: "global" | "project" | "mode" // For hooksOpenConfigFolder, hooksDeleteHook
hookUpdates?: {
events?: string[]
matcher?: string
id?: string
command?: string
enabled?: boolean
description?: string
shell?: string
includeConversationHistory?: boolean
timeout?: number
} // For hooksUpdateHook
filePath?: string // For hooksOpenHookFile
codeIndexSettings?: {
// Global state settings
codebaseIndexEnabled: boolean
Expand Down
Loading