InkdownInkdown
Start writing

Claude-Code

62 files·4 subfolders

Shared Workspace

Claude-Code
codex

20-hooks-system

Shared from "Claude-Code" on Inkdown

Hooks System: Pre/Post Sampling Architecture

Overview

The Hooks System is Claude Code's extension mechanism for modifying behavior without changing core code. It provides interception points before and after key operations, enabling git tracking, metrics, safety checks, and custom extensions.

Plain text
┌─────────────────────────────────────────────────────────────────────────────┐
│                    HOOKS SYSTEM ARCHITECTURE                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                    HOOK TYPES                                        │   │
│  │                                                                      │   │
│  │  PRE-SAMPLING HOOKS                                                  │   │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐            │   │
│  │  │ pre_tool │  │ pre_edit │  │ pre_bash │  │ pre_git  │            │   │
│  │  │ _sample  │  │ _sample  │  │ _sample  │  │ _sample  │            │   │
│  │  └──────────┘  └──────────┘  └──────────┘  └──────────┘            │   │
│  │       │            │            │            │                     │   │
│  │       ▼            ▼            ▼            ▼                     │   │
│  │  Run BEFORE the operation ──► Can cancel, modify, or log           │   │
│  │                                                                      │   │
│  ├────────────────────────────────────────────────────────────────────│   │
│  │                                                                      │   │
│  │  POST-SAMPLING HOOKS                                                 │   │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐            │   │
│  │  │post_tool │  │post_edit │  │post_bash │  │post_git  │            │   │
│  │  │_sample   │  │_sample   │  │_sample   │  │_sample   │            │   │
│  │  └──────────┘  └──────────┘  └──────────┘  └──────────┘            │   │
│  │       │            │            │            │                     │   │
│  │       ▼            ▼            ▼            ▼                     │   │
│  │  Run AFTER the operation ──► Record result, update state          │   │
│  │                                                                      │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                    HOOK EXECUTION FLOW                               │   │
│  │                                                                      │   │
│  │   1. Tool execution requested                                        │   │
│  │        │                                                             │   │
│  │        ▼                                                             │   │
│  │   2. Run all PRE hooks (sequential)                                 │   │
│  │      ┌────────┐   ┌────────┐   ┌────────┐                          │   │
│  │      │pre_tool│──►│pre_edit│──►│pre_bash│──► ...                    │   │
│  │      │_sample │   │_sample │   │_sample │                          │   │
│  │      └────────┘   └────────┘   └────────┘                          │   │
│  │           │                                                        │   │
│  │      If any returns │cancel│ ──► Abort operation                   │   │
│  │                                                                      │   │
│  │        ▼                                                             │   │
│  │   3. Execute the actual tool                                        │   │
│  │        │                                                             │   │
│  │        ▼                                                             │   │
│  │   4. Run all POST hooks (parallel-safe)                             │   │
│  │      ┌────────┐   ┌────────┐   ┌────────┐                          │   │
│  │      │post_tool│   │post_edit│   │post_git│                       │   │
│  │      │_sample │   │_sample │   │_sample │                          │   │
│  │      └────────┘   └────────┘   └────────┘                          │   │
│  │           │            │            │                               │   │
│  │           └────────────┴────────────┘                               │   │
│  │                      │                                              │   │
│  │                      ▼                                              │   │
│  │   5. Continue with next operation                                   │   │
│  │                                                                      │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
0000_start_here_index_and_recommended_reading_order.md
0100_project_overview_tech_stack_runtime_modes_and_folder_map.md
0200_startup_flow_entry_points_and_cold_start_sequence.md
0300_codebase_modules_layers_state_models_and_schemas.md
0400_system_architecture_and_design_rationale.md
0500_interactive_repl_request_flow_end_to_end.md
0600_headless_sdk_and_print_mode_request_flow_end_to_end.md
0700_mcp_integration_connection_and_tool_call_flow.md
0800_external_services_sdks_storage_and_local_dependencies.md
0900_environment_variables_settings_feature_flags_and_failure_modes.md
1000_non_obvious_patterns_gotchas_and_debugging_traps.md
1100_full_codebase_file_inventory_grouped_by_directory.md
kimi
00-overview.md
01-entrypoints.md
02-state-management.md
03-query-system.md
04-tools-system.md
05-tasks-system.md
06-ui-components.md
07-bridge-remote.md
08-services.md
09-skills-plugins.md
10-commands.md
11-testing-architecture.md
12-permission-system.md
13-build-system.md
14-ink-internals.md
15-git-internals.md
16-context-compaction.md
17-vim-mode.md
18-mailbox-notifications.md
19-session-persistence.md
20-hooks-system.md
21-error-recovery.md
README.md
qwen
00-overview.md
01-entry-points.md
02-query-engine.md
03-tools-and-tasks.md
04-commands-and-skills.md
05-state-management.md
06-ink-rendering.md
07-bridge-remote.md
08-mcp-services.md
09-services-overview.md
10-multi-agent.md
11-system-prompt-constants.md
12-tool-interface.md
13-memory-system.md
14-buddy-companion.md
15-keybindings.md
16-stop-hooks.md
17-vim-mode.md
18-upstreamproxy.md
19-cost-tracking-history.md
20-contexts-styles-onboarding.md
21-hooks.md
22-screens.md
tweets-explain
claude-code-memory-analysis.md
compact
memory-system
agentic-architecture

Core Files

FilePurpose
types/hooks.tsHook type definitions
utils/hooks/hookHelpers.tsHook registration and execution
utils/hooks/postSamplingHooks.tsPost-sampling hook implementation
utils/hooks/preSamplingHooks.tsPre-sampling hook implementation
utils/hooks/sessionHooks.tsSession lifecycle hooks
utils/hooks/hookEvents.tsHook event bus
tools/shared/gitOperationTracking.tsGit tracking hooks

Hook Interface

TypeScript
// types/hooks.ts
export type HookType =
  // Pre-sampling hooks
  | 'pre_tool_sample'
  | 'pre_edit_sample'
  | 'pre_bash_sample'
  | 'pre_git_sample'
  | 'pre_agent_sample'
  | 'pre_compact_sample'

  // Post-sampling hooks
  | 'post_tool_sample'
  | 'post_edit_sample'
  | 'post_bash_sample'
  | 'post_git_sample'
  | 'post_agent_sample'
  | 'post_compact_sample'

  // Session hooks
  | 'session_start'
  | 'session_end'
  | 'query_start'
  | 'query_end'

export type HookResult =
  | { status: 'continue' }  // Proceed with operation
  | { status: 'cancel'; reason: string }  // Abort operation
  | { status: 'modify'; data: unknown }  // Modify operation data
  | { status: 'defer'; promise: Promise<HookResult> }  // Async decision

export type HookHandler<T = unknown> = (
  context: T,
  event: HookEvent
) => HookResult | Promise<HookResult>

type HookEvent = {
  type: HookType
  timestamp: number
  sessionId: string
  toolUseId?: string
}

Hook Registration

TypeScript
// utils/hooks/hookHelpers.ts
const hookRegistry = new Map<HookType, Set<HookHandler>>()

export function registerHook(
  type: HookType,
  handler: HookHandler,
  options: { priority?: number } = {}
): () => void {
  if (!hookRegistry.has(type)) {
    hookRegistry.set(type, new Set())
  }

  const handlers = hookRegistry.get(type)!
  handlers.add(handler)

  // Return unregister function
  return () => {
    handlers.delete(handler)
  }
}

export async function executeHooks(
  type: HookType,
  context: unknown,
  event: HookEvent
): Promise<HookResult> {
  const handlers = hookRegistry.get(type)

  if (!handlers || handlers.size === 0) {
    return { status: 'continue' }
  }

  // Execute sequentially (order matters)
  for (const handler of handlers) {
    try {
      const result = await handler(context, event)

      if (result.status === 'cancel') {
        return result  // First cancellation wins
      }

      if (result.status === 'modify') {
        // Update context for next hook
        context = result.data
      }

      if (result.status === 'defer') {
        // Wait for async decision
        const deferred = await result.promise
        if (deferred.status === 'cancel') {
          return deferred
        }
        if (deferred.status === 'modify') {
          context = deferred.data
        }
      }
    } catch (error) {
      logError(`Hook ${type} failed`, error)
      // Continue with other hooks, but log the failure
    }
  }

  return { status: 'continue', data: context }
}

Pre-Sampling Hooks

Git Tracking Hook (Pre-Edit)
TypeScript
// tools/shared/gitOperationTracking.ts
export function registerGitTrackingHooks(): void {
  // Before file edit: snapshot original state
  registerHook('pre_edit_sample', async (context: EditContext, event) => {
    const { file_path } = context

    // Read original content
    const originalContent = await readFile(file_path, 'utf-8')

    // Store in hook context (passed to post hook)
    context._originalContent = originalContent

    // Record in attribution state
    const { setAppState } = getGlobalContext()
    setAppState(prev => ({
      ...prev,
      fileHistory: {
        ...prev.fileHistory,
        snapshots: new Map([
          ...prev.fileHistory.snapshots,
          [file_path, originalContent],
        ]),
      },
    }))

    return { status: 'continue', data: context }
  })

  // After file edit: record what changed
  registerHook('post_edit_sample', async (context: EditContext, event) => {
    const { file_path, _originalContent } = context

    // Read new content
    const newContent = await readFile(file_path, 'utf-8')

    // If content changed, record attribution
    if (_originalContent !== newContent) {
      recordAttribution(file_path, _originalContent, newContent)

      // Trigger git status update
      getMailbox()?.send({
        type: 'file_changed',
        path: file_path,
        change: 'modified',
      })
    }

    return { status: 'continue' }
  })
}
Permission Check Hook (Pre-Tool)
TypeScript
// utils/permissions/hookIntegration.ts
export function registerPermissionHooks(): void {
  registerHook('pre_tool_sample', async (context: ToolContext, event) => {
    const { tool, input, toolPermissionContext } = context

    // Run permission check
    const result = await checkPermission(tool, input, toolPermissionContext)

    if (!result.allowed) {
      return {
        status: 'cancel',
        reason: result.reason || 'Permission denied',
      }
    }

    // Add permission metadata to context
    context._permissionResult = result

    return { status: 'continue', data: context }
  })
}
Safety Hook (Pre-Bash)
TypeScript
// utils/safety/hooks.ts
export function registerSafetyHooks(): void {
  registerHook('pre_bash_sample', async (context: BashContext, event) => {
    const { command } = context.input

    // Check for dangerous patterns
    const dangerousPatterns = [
      { pattern: /rm -rf\s+\//, reason: 'Attempted root deletion' },
      { pattern: />:\s*\//, reason: 'Potential system overwrite' },
      { pattern: /curl.*\|\s*sh/, reason: 'Remote code execution' },
    ]

    for (const { pattern, reason } of dangerousPatterns) {
      if (pattern.test(command)) {
        return {
          status: 'cancel',
          reason: `Dangerous command blocked: ${reason}`,
        }
      }
    }

    // Check for sudo (extra confirmation)
    if (command.startsWith('sudo ')) {
      const confirmed = await confirmWithUser(
        `This command uses sudo: ${command}\n\nProceed?`
      )

      if (!confirmed) {
        return {
          status: 'cancel',
          reason: 'User cancelled sudo command',
        }
      }
    }

    return { status: 'continue' }
  })
}

Post-Sampling Hooks

Metrics Hook
TypeScript
// utils/telemetry/hooks.ts
export function registerTelemetryHooks(): void {
  registerHook('post_tool_sample', async (context: ToolContext, event) => {
    const { tool, result, duration } = context

    // Record metrics
    logEvent('tool_executed', {
      tool_name: tool.name,
      duration_ms: duration,
      success: !result.is_error,
      error_type: result.is_error ? result.content.slice(0, 100) : undefined,
    })

    return { status: 'continue' }
  })

  registerHook('post_bash_sample', async (context: BashContext, event) => {
    const { input, result } = context

    // Track command patterns
    const command = input.command
    const baseCommand = command.split(' ')[0]

    logEvent('bash_executed', {
      command_base: baseCommand,
      has_pipe: command.includes('|'),
      has_redirect: command.includes('>') || command.includes('<'),
      exit_code: result.exitCode,
      duration_ms: context.duration,
    })

    return { status: 'continue' }
  })
}
File History Hook
TypeScript
// utils/fileHistory/hooks.ts
export function registerFileHistoryHooks(): void {
  registerHook('post_edit_sample', async (context: EditContext, event) => {
    const { file_path, _originalContent } = context

    // Update file history
    const historyEntry: FileHistoryEntry = {
      timestamp: Date.now(),
      sessionId: event.sessionId,
      toolUseId: event.toolUseId,
      type: 'edit',
      sizeDelta: context.newContent.length - _originalContent.length,
    }

    await appendFileHistory(file_path, historyEntry)

    return { status: 'continue' }
  })
}
Auto-Compact Hook
TypeScript
// services/compact/autoCompactHooks.ts
export function registerAutoCompactHooks(): void {
  registerHook('post_tool_sample', async (context, event) => {
    const { getAppState } = getGlobalContext()
    const state = getAppState()

    // Check if we should auto-compact
    if (shouldAutoCompact(state.messages)) {
      // Show notification
      showNotification({
        type: 'info',
        title: 'Compacting conversation...',
        timeout: 3000,
      })

      // Trigger compaction (async, non-blocking)
      queueMicrotask(() => {
        triggerAutoCompact(state, getGlobalContext().setAppState)
      })
    }

    return { status: 'continue' }
  })
}

Session Lifecycle Hooks

TypeScript
// utils/hooks/sessionHooks.ts
export type SessionHooksState = {
  onStart: Array<() => void>
  onEnd: Array<() => void>
}

export function executeSessionStartHooks(): void {
  const hooks = getSessionHooks()

  for (const hook of hooks.onStart) {
    try {
      hook()
    } catch (error) {
      logError('Session start hook failed', error)
    }
  }
}

export function executeSessionEndHooks(): void {
  const hooks = getSessionHooks()

  // Run in reverse order (cleanup)
  for (const hook of hooks.onEnd.slice().reverse()) {
    try {
      hook()
    } catch (error) {
      logError('Session end hook failed', error)
    }
  }
}

// Example: Register cleanup
export function withCleanup(
  setup: () => void,
  cleanup: () => void
): () => void {
  const state = getSessionHooksState()
  state.onEnd.push(cleanup)
  setup()

  return () => {
    const index = state.onEnd.indexOf(cleanup)
    if (index >= 0) {
      state.onEnd.splice(index, 1)
    }
    cleanup()
  }
}

Hook Integration in Tool Execution

TypeScript
// Tool.ts - Hook integration point
export async function executeToolWithHooks(
  tool: Tool,
  input: unknown,
  context: ToolUseContext
): Promise<ToolResult> {
  const event: HookEvent = {
    type: 'pre_tool_sample',
    timestamp: Date.now(),
    sessionId: getSessionId(),
    toolUseId: generateUUID(),
  }

  // 1. Pre-hooks
  const hookContext = { tool, input, context }
  const preResult = await executeHooks('pre_tool_sample', hookContext, event)

  if (preResult.status === 'cancel') {
    return {
      type: 'tool_result',
      content: preResult.reason || 'Cancelled by hook',
      is_error: true,
    }
  }

  // Use modified context if provided
  const finalContext = preResult.status === 'modify'
    ? preResult.data
    : hookContext

  // 2. Execute tool
  const startTime = performance.now()
  const result = await tool.execute(finalContext.input, context)
  const duration = performance.now() - startTime

  // 3. Post-hooks
  const postContext = { ...finalContext, result, duration }
  const postResult = await executeHooks(
    'post_tool_sample',
    postContext,
    { ...event, type: 'post_tool_sample' }
  )

  // Post-hooks can modify result
  if (postResult.status === 'modify') {
    return postResult.data
  }

  return result
}

Key Design Principles

  1. Order Matters: Pre-hooks execute sequentially, first cancellation wins
  2. Context Mutation: Hooks can modify context for downstream execution
  3. Error Isolation: One failing hook doesn't break others
  4. Async Support: Hooks can defer decisions (e.g., waiting for user)
  5. Cleanup: Post-hooks run even if operation fails (try/finally)

Debugging Hooks

Bash
# Log all hook execution
DEBUG=hooks claude

# See registered hooks
claude /debug hooks

# Disable specific hooks
CLAUDE_CODE_DISABLE_HOOKS=git_tracking,safety_checks claude