π File detail
utils/hooks/hooksConfigManager.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes MatcherMetadata, HookEventMetadata, getHookEventMetadata, groupHooksByEventAndMatcher, and getSortedMatchersForEvent (and more) β mainly functions, hooks, or classes. Dependencies touch lodash-es and src. It composes internal code from bootstrap, state, and hooksSettings (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import memoize from 'lodash-es/memoize.js' import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js' import { getRegisteredHooks } from '../../bootstrap/state.js' import type { AppState } from '../../state/AppState.js' import {
π€ Exports (heuristic)
MatcherMetadataHookEventMetadatagetHookEventMetadatagroupHooksByEventAndMatchergetSortedMatchersForEventgetHooksForMatchergetMatcherMetadata
π External import roots
Package roots from from "β¦" (relative paths omitted).
lodash-essrc
π₯οΈ Source preview
import memoize from 'lodash-es/memoize.js'
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'
import { getRegisteredHooks } from '../../bootstrap/state.js'
import type { AppState } from '../../state/AppState.js'
import {
getAllHooks,
type IndividualHookConfig,
sortMatchersByPriority,
} from './hooksSettings.js'
export type MatcherMetadata = {
fieldToMatch: string
values: string[]
}
export type HookEventMetadata = {
summary: string
description: string
matcherMetadata?: MatcherMetadata
}
// Hook event metadata configuration.
// Resolver uses sorted-joined string key so that callers passing a fresh
// toolNames array each render (e.g. HooksConfigMenu) hit the cache instead
// of leaking a new entry per call.
export const getHookEventMetadata = memoize(
function (toolNames: string[]): Record<HookEvent, HookEventMetadata> {
return {
PreToolUse: {
summary: 'Before tool execution',
description:
'Input to command is JSON of tool call arguments.\nExit code 0 - stdout/stderr not shown\nExit code 2 - show stderr to model and block tool call\nOther exit codes - show stderr to user only but continue with tool call',
matcherMetadata: {
fieldToMatch: 'tool_name',
values: toolNames,
},
},
PostToolUse: {
summary: 'After tool execution',
description:
'Input to command is JSON with fields "inputs" (tool call arguments) and "response" (tool call response).\nExit code 0 - stdout shown in transcript mode (ctrl+o)\nExit code 2 - show stderr to model immediately\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'tool_name',
values: toolNames,
},
},
PostToolUseFailure: {
summary: 'After tool execution fails',
description:
'Input to command is JSON with tool_name, tool_input, tool_use_id, error, error_type, is_interrupt, and is_timeout.\nExit code 0 - stdout shown in transcript mode (ctrl+o)\nExit code 2 - show stderr to model immediately\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'tool_name',
values: toolNames,
},
},
PermissionDenied: {
summary: 'After auto mode classifier denies a tool call',
description:
'Input to command is JSON with tool_name, tool_input, tool_use_id, and reason.\nReturn {"hookSpecificOutput":{"hookEventName":"PermissionDenied","retry":true}} to tell the model it may retry.\nExit code 0 - stdout shown in transcript mode (ctrl+o)\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'tool_name',
values: toolNames,
},
},
Notification: {
summary: 'When notifications are sent',
description:
'Input to command is JSON with notification message and type.\nExit code 0 - stdout/stderr not shown\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'notification_type',
values: [
'permission_prompt',
'idle_prompt',
'auth_success',
'elicitation_dialog',
'elicitation_complete',
'elicitation_response',
],
},
},
UserPromptSubmit: {
summary: 'When the user submits a prompt',
description:
'Input to command is JSON with original user prompt text.\nExit code 0 - stdout shown to Claude\nExit code 2 - block processing, erase original prompt, and show stderr to user only\nOther exit codes - show stderr to user only',
},
SessionStart: {
summary: 'When a new session is started',
description:
'Input to command is JSON with session start source.\nExit code 0 - stdout shown to Claude\nBlocking errors are ignored\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'source',
values: ['startup', 'resume', 'clear', 'compact'],
},
},
Stop: {
summary: 'Right before Claude concludes its response',
description:
'Exit code 0 - stdout/stderr not shown\nExit code 2 - show stderr to model and continue conversation\nOther exit codes - show stderr to user only',
},
StopFailure: {
summary: 'When the turn ends due to an API error',
description:
'Fires instead of Stop when an API error (rate limit, auth failure, etc.) ended the turn. Fire-and-forget β hook output and exit codes are ignored.',
matcherMetadata: {
fieldToMatch: 'error',
values: [
'rate_limit',
'authentication_failed',
'billing_error',
'invalid_request',
'server_error',
'max_output_tokens',
'unknown',
],
},
},
SubagentStart: {
summary: 'When a subagent (Agent tool call) is started',
description:
'Input to command is JSON with agent_id and agent_type.\nExit code 0 - stdout shown to subagent\nBlocking errors are ignored\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'agent_type',
values: [], // Will be populated with available agent types
},
},
SubagentStop: {
summary:
'Right before a subagent (Agent tool call) concludes its response',
description:
'Input to command is JSON with agent_id, agent_type, and agent_transcript_path.\nExit code 0 - stdout/stderr not shown\nExit code 2 - show stderr to subagent and continue having it run\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'agent_type',
values: [], // Will be populated with available agent types
},
},
PreCompact: {
summary: 'Before conversation compaction',
description:
'Input to command is JSON with compaction details.\nExit code 0 - stdout appended as custom compact instructions\nExit code 2 - block compaction\nOther exit codes - show stderr to user only but continue with compaction',
matcherMetadata: {
fieldToMatch: 'trigger',
values: ['manual', 'auto'],
},
},
PostCompact: {
summary: 'After conversation compaction',
description:
'Input to command is JSON with compaction details and the summary.\nExit code 0 - stdout shown to user\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'trigger',
values: ['manual', 'auto'],
},
},
SessionEnd: {
summary: 'When a session is ending',
description:
'Input to command is JSON with session end reason.\nExit code 0 - command completes successfully\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'reason',
values: ['clear', 'logout', 'prompt_input_exit', 'other'],
},
},
PermissionRequest: {
summary: 'When a permission dialog is displayed',
description:
'Input to command is JSON with tool_name, tool_input, and tool_use_id.\nOutput JSON with hookSpecificOutput containing decision to allow or deny.\nExit code 0 - use hook decision if provided\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'tool_name',
values: toolNames,
},
},
Setup: {
summary: 'Repo setup hooks for init and maintenance',
description:
'Input to command is JSON with trigger (init or maintenance).\nExit code 0 - stdout shown to Claude\nBlocking errors are ignored\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'trigger',
values: ['init', 'maintenance'],
},
},
TeammateIdle: {
summary: 'When a teammate is about to go idle',
description:
'Input to command is JSON with teammate_name and team_name.\nExit code 0 - stdout/stderr not shown\nExit code 2 - show stderr to teammate and prevent idle (teammate continues working)\nOther exit codes - show stderr to user only',
},
TaskCreated: {
summary: 'When a task is being created',
description:
'Input to command is JSON with task_id, task_subject, task_description, teammate_name, and team_name.\nExit code 0 - stdout/stderr not shown\nExit code 2 - show stderr to model and prevent task creation\nOther exit codes - show stderr to user only',
},
TaskCompleted: {
summary: 'When a task is being marked as completed',
description:
'Input to command is JSON with task_id, task_subject, task_description, teammate_name, and team_name.\nExit code 0 - stdout/stderr not shown\nExit code 2 - show stderr to model and prevent task completion\nOther exit codes - show stderr to user only',
},
Elicitation: {
summary: 'When an MCP server requests user input (elicitation)',
description:
'Input to command is JSON with mcp_server_name, message, and requested_schema.\nOutput JSON with hookSpecificOutput containing action (accept/decline/cancel) and optional content.\nExit code 0 - use hook response if provided\nExit code 2 - deny the elicitation\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'mcp_server_name',
values: [],
},
},
ElicitationResult: {
summary: 'After a user responds to an MCP elicitation',
description:
'Input to command is JSON with mcp_server_name, action, content, mode, and elicitation_id.\nOutput JSON with hookSpecificOutput containing optional action and content to override the response.\nExit code 0 - use hook response if provided\nExit code 2 - block the response (action becomes decline)\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'mcp_server_name',
values: [],
},
},
ConfigChange: {
summary: 'When configuration files change during a session',
description:
'Input to command is JSON with source (user_settings, project_settings, local_settings, policy_settings, skills) and file_path.\nExit code 0 - allow the change\nExit code 2 - block the change from being applied to the session\nOther exit codes - show stderr to user only',
matcherMetadata: {
fieldToMatch: 'source',
values: [
'user_settings',
'project_settings',
'local_settings',
'policy_settings',
'skills',
],
},
},
InstructionsLoaded: {
summary: 'When an instruction file (CLAUDE.md or rule) is loaded',
description:
'Input to command is JSON with file_path, memory_type (User, Project, Local, Managed), load_reason (session_start, nested_traversal, path_glob_match, include, compact), globs (optional β the paths: frontmatter patterns that matched), trigger_file_path (optional β the file Claude touched that caused the load), and parent_file_path (optional β the file that @-included this one).\nExit code 0 - command completes successfully\nOther exit codes - show stderr to user only\nThis hook is observability-only and does not support blocking.',
matcherMetadata: {
fieldToMatch: 'load_reason',
values: [
'session_start',
'nested_traversal',
'path_glob_match',
'include',
'compact',
],
},
},
WorktreeCreate: {
summary: 'Create an isolated worktree for VCS-agnostic isolation',
description:
'Input to command is JSON with name (suggested worktree slug).\nStdout should contain the absolute path to the created worktree directory.\nExit code 0 - worktree created successfully\nOther exit codes - worktree creation failed',
},
WorktreeRemove: {
summary: 'Remove a previously created worktree',
description:
'Input to command is JSON with worktree_path (absolute path to worktree).\nExit code 0 - worktree removed successfully\nOther exit codes - show stderr to user only',
},
CwdChanged: {
summary: 'After the working directory changes',
description:
'Input to command is JSON with old_cwd and new_cwd.\nCLAUDE_ENV_FILE is set β write bash exports there to apply env to subsequent BashTool commands.\nHook output can include hookSpecificOutput.watchPaths (array of absolute paths) to register with the FileChanged watcher.\nExit code 0 - command completes successfully\nOther exit codes - show stderr to user only',
},
FileChanged: {
summary: 'When a watched file changes',
description:
'Input to command is JSON with file_path and event (change, add, unlink).\nCLAUDE_ENV_FILE is set β write bash exports there to apply env to subsequent BashTool commands.\nThe matcher field specifies filenames to watch in the current directory (e.g. ".envrc|.env").\nHook output can include hookSpecificOutput.watchPaths (array of absolute paths) to dynamically update the watch list.\nExit code 0 - command completes successfully\nOther exit codes - show stderr to user only',
},
}
},
toolNames => toolNames.slice().sort().join(','),
)
// Group hooks by event and matcher
export function groupHooksByEventAndMatcher(
appState: AppState,
toolNames: string[],
): Record<HookEvent, Record<string, IndividualHookConfig[]>> {
const grouped: Record<HookEvent, Record<string, IndividualHookConfig[]>> = {
PreToolUse: {},
PostToolUse: {},
PostToolUseFailure: {},
PermissionDenied: {},
Notification: {},
UserPromptSubmit: {},
SessionStart: {},
SessionEnd: {},
Stop: {},
StopFailure: {},
SubagentStart: {},
SubagentStop: {},
PreCompact: {},
PostCompact: {},
PermissionRequest: {},
Setup: {},
TeammateIdle: {},
TaskCreated: {},
TaskCompleted: {},
Elicitation: {},
ElicitationResult: {},
ConfigChange: {},
WorktreeCreate: {},
WorktreeRemove: {},
InstructionsLoaded: {},
CwdChanged: {},
FileChanged: {},
}
const metadata = getHookEventMetadata(toolNames)
// Include hooks from settings files
getAllHooks(appState).forEach(hook => {
const eventGroup = grouped[hook.event]
if (eventGroup) {
// For events without matchers, use empty string as key
const matcherKey =
metadata[hook.event].matcherMetadata !== undefined
? hook.matcher || ''
: ''
if (!eventGroup[matcherKey]) {
eventGroup[matcherKey] = []
}
eventGroup[matcherKey].push(hook)
}
})
// Include registered hooks (e.g., plugin hooks)
const registeredHooks = getRegisteredHooks()
if (registeredHooks) {
for (const [event, matchers] of Object.entries(registeredHooks)) {
const hookEvent = event as HookEvent
const eventGroup = grouped[hookEvent]
if (!eventGroup) continue
for (const matcher of matchers) {
const matcherKey = matcher.matcher || ''
// Only PluginHookMatcher has pluginRoot; HookCallbackMatcher (internal
// callbacks like attributionHooks, sessionFileAccessHooks) does not.
if ('pluginRoot' in matcher) {
eventGroup[matcherKey] ??= []
for (const hook of matcher.hooks) {
eventGroup[matcherKey].push({
event: hookEvent,
config: hook,
matcher: matcher.matcher,
source: 'pluginHook',
pluginName: matcher.pluginId,
})
}
} else if (process.env.USER_TYPE === 'ant') {
eventGroup[matcherKey] ??= []
for (const _hook of matcher.hooks) {
eventGroup[matcherKey].push({
event: hookEvent,
config: {
type: 'command',
command: '[ANT-ONLY] Built-in Hook',
},
matcher: matcher.matcher,
source: 'builtinHook',
})
}
}
}
}
}
return grouped
}
// Get sorted matchers for a specific event
export function getSortedMatchersForEvent(
hooksByEventAndMatcher: Record<
HookEvent,
Record<string, IndividualHookConfig[]>
>,
event: HookEvent,
): string[] {
const matchers = Object.keys(hooksByEventAndMatcher[event] || {})
return sortMatchersByPriority(matchers, hooksByEventAndMatcher, event)
}
// Get hooks for a specific event and matcher
export function getHooksForMatcher(
hooksByEventAndMatcher: Record<
HookEvent,
Record<string, IndividualHookConfig[]>
>,
event: HookEvent,
matcher: string | null,
): IndividualHookConfig[] {
// For events without matchers, hooks are stored with empty string as key
// because the record keys must be strings.
const matcherKey = matcher ?? ''
return hooksByEventAndMatcher[event]?.[matcherKey] ?? []
}
// Get metadata for a specific event's matcher
export function getMatcherMetadata(
event: HookEvent,
toolNames: string[],
): MatcherMetadata | undefined {
return getHookEventMetadata(toolNames)[event].matcherMetadata
}