Claude Code Hooks与安全机制深度解析

作者:小学子 📚  |  日期:2026年4月1日  |  基于 Claude Code v2.1.88 源码


前言

Claude Code 的技能系统不仅仅是一个提示词执行器,它还包含了强大的生命周期钩子系统多层安全模型。本文深入解析这两个核心组件。


一、Hooks 系统概述

1.1 什么是 Hooks

Hooks 允许在技能执行的关键时刻注入自定义逻辑,实现:

  • 自动验证(如测试后检查)
  • 通知(如操作后发送通知)
  • 安全控制(如危险命令确认)
  • 流程自动化(如提交前 lint)
  • 1.2 28 种生命周期事件

    类别事件说明
    **工具执行**`PreToolUse`工具执行前
    `PostToolUse`工具执行后
    `PostToolUseFailure`工具执行失败
    **会话周期**`SessionStart`会话开始
    `SessionEnd`会话结束
    `Setup`初始化完成
    **代理事件**`SubagentStart`子代理开始
    `SubagentStop`子代理停止
    `TeammateIdle`队友空闲
    **任务事件**`TaskCreated`任务创建
    `TaskCompleted`任务完成
    **用户交互**`UserPromptSubmit`用户提交输入
    `Elicitation`信息请求
    `ElicitationResult`信息请求结果
    `Notification`通知
    **Compact**`PreCompact`上下文压缩前
    `PostCompact`上下文压缩后
    **权限**`PermissionRequest`权限请求
    `PermissionDenied`权限拒绝
    **系统**`Stop`停止
    `StopFailure`停止失败
    `ConfigChange`配置变更
    **文件系统**`WorktreeCreate`Worktree 创建
    `WorktreeRemove`Worktree 删除
    `CwdChanged`工作目录变更
    `FileChanged`文件变更
    **指令**`InstructionsLoaded`指令加载完成


    二、四种钩子类型

    2.1 Bash 命令钩子

    
    {
      type: 'command',
      command: 'echo "Running: $TOOL_NAME"',
      if: 'Bash(git commit:*)',    // 条件过滤
      shell: 'bash',                 // shell 类型
      timeout: 30,                  // 超时(秒)
      statusMessage: 'Running hook...',
      once: false,                   // 是否只执行一次
      async: false,                  // 是否异步执行
    }
    

    2.2 LLM 提示词钩子

    
    {
      type: 'prompt',
      prompt: 'Should we allow $ARGUMENTS? Reply yes or no.',
      if: 'Write(*.env)',
      model: 'claude-haiku-4',
      timeout: 60,
    }
    

    2.3 HTTP 钩子

    
    {
      type: 'http',
      url: 'https://webhook.example.com/notify',
      if: 'Bash(rm *)',
      headers: { 'Authorization': 'Bearer $WEBHOOK_TOKEN' },
      allowedEnvVars: ['WEBHOOK_TOKEN'],
      timeout: 10,
    }
    

    2.4 Agent 验证钩子

    
    {
      type: 'agent',
      prompt: 'Verify that unit tests ran and passed.',
      if: 'Bash(npm test:*)',
      model: 'claude-sonnet-4-6',
      timeout: 120,
    }
    

    三、技能中的 Hooks 声明

    3.1 Frontmatter 格式

    
    ---
    name: my-workflow
    hooks:
      PostToolUse:
        - if: "Bash(npm test)"
          type: prompt
          prompt: "Tests passed. Should we add coverage?"
      PreToolUse:
        - if: "Write(*.ts)"
          type: command
          command: "echo 'Editing TypeScript file'"
    ---
    

    3.2 解析与验证

    
    // src/skills/loadSkillsDir.ts
    function parseHooksFromFrontmatter(
      frontmatter: FrontmatterData,
      skillName: string,
    ): HooksSettings | undefined {
      if (!frontmatter.hooks) {
        return undefined
      }
    
      // 使用 HooksSchema 验证
      const result = HooksSchema().safeParse(frontmatter.hooks)
      if (!result.success) {
        logForDebugging(`Invalid hooks in skill '${skillName}': ${result.error.message}`)
        return undefined
      }
    
      return result.data
    }
    

    四、Hooks 注册流程

    4.1 注册函数

    
    export function registerSkillHooks(
      setAppState,
      sessionId: string,
      hooks: HooksSettings,
      skillName: string,
      skillRoot?: string,
    ): void {
      for (const eventName of HOOK_EVENTS) {
        const matchers = hooks[eventName]
        if (!matchers) continue
    
        for (const matcher of matchers) {
          for (const hook of matcher.hooks) {
            // once: true 的钩子执行后自动移除
            const onHookSuccess = hook.once
              ? () => removeSessionHook(setAppState, sessionId, eventName, hook)
              : undefined
    
            addSessionHook(
              setAppState,
              sessionId,
              eventName,
              matcher.matcher || '',
              hook,
              onHookSuccess,
              skillRoot,
            )
          }
        }
      }
    }
    

    五、条件匹配语法

    5.1 基础匹配

    
    // 精确匹配
    if: 'Bash(git commit)'      // git commit 命令
    
    // 通配符匹配
    if: 'Bash(git *)'           // 所有 git 命令
    if: 'Write(*.env)'           // 写入 .env 文件
    if: 'Read(*.ts)'             // 读取 TypeScript 文件
    if: 'Bash(rm -rf *)'        // 危险操作
    

    5.2 规则解析

    
    // src/utils/hooks/permissionMatcher.ts
    function matchesToolCommand(
      pattern: string,
      toolName: string,
      toolArgs: string,
    ): boolean {
      const normalizedPattern = pattern.startsWith('/')
        ? pattern.substring(1)
        : pattern
    
      const fullCommand = toolArgs ? `${toolName}(${toolArgs})` : toolName
    
      // 精确匹配
      if (normalizedPattern === fullCommand) {
        return true
      }
    
      // 通配符匹配
      if (normalizedPattern.endsWith('(*)')) {
        const prefix = normalizedPattern.slice(0, -3)
        return fullCommand.startsWith(prefix)
      }
    
      return false
    }
    

    六、安全模型

    6.1 多层权限检查

    
    // 1. deny 规则检查(优先级最高)
    const denyRules = getRuleByContentsForTool(permissionContext, SkillTool, 'deny')
    for (const [ruleContent, rule] of denyRules) {
      if (ruleMatches(ruleContent)) {
        return { behavior: 'deny', ... }
      }
    }
    
    // 2. 远程规范技能自动放行
    if (isRemoteCanonicalSkill) {
      return { behavior: 'allow', ... }
    }
    
    // 3. allow 规则检查
    const allowRules = getRuleByContentsForTool(permissionContext, SkillTool, 'allow')
    for (const [ruleContent, rule] of allowRules) {
      if (ruleMatches(ruleContent)) {
        return { behavior: 'allow', ... }
      }
    }
    
    // 4. 安全属性自动放行
    if (skillHasOnlySafeProperties(command)) {
      return { behavior: 'allow', ... }
    }
    
    // 5. 默认:询问用户
    return { behavior: 'ask', suggestions: [...] }
    

    6.2 SAFE_SKILL_PROPERTIES 白名单

    
    const SAFE_SKILL_PROPERTIES = new Set([
      // PromptCommand properties
      'type',
      'progressMessage',
      'contentLength',
      'argNames',
      'model',
      'effort',
      'source',
      'pluginInfo',
      'disableNonInteractive',
      'skillRoot',
      'context',
      'agent',
      'getPromptForCommand',
      'frontmatterKeys',
      // CommandBase properties
      'name',
      'description',
      'hasUserSpecifiedDescription',
      'isEnabled',
      'isHidden',
      'aliases',
      'isMcp',
      'argumentHint',
      'whenToUse',
      'paths',
      'version',
      'disableModelInvocation',
      'userInvocable',
      'loadedFrom',
      'immediate',
      'userFacingName',
    ])
    

    6.3 MCP 技能沙箱隔离

    
    // MCP 技能禁止执行 shell 命令
    if (loadedFrom !== 'mcp') {
      finalContent = await executeShellCommandsInPrompt(finalContent, ...)
    }
    

    七、来源标注系统

    7.1 formatDescriptionWithSource

    
    export function formatDescriptionWithSource(cmd: Command): string {
      if (cmd.type !== 'prompt') {
        return cmd.description
      }
    
      // 工作流类型
      if (cmd.kind === 'workflow') {
        return `${cmd.description} (workflow)`
      }
    
      // 插件技能
      if (cmd.source === 'plugin') {
        const pluginName = cmd.pluginInfo?.pluginManifest.name
        if (pluginName) {
          return `(${pluginName}) ${cmd.description}`
        }
      }
    
      // 内置技能
      if (cmd.source === 'bundled') {
        return `${cmd.description} (bundled)`
      }
    
      return `${cmd.description} (${getSettingSourceName(cmd.source)})`
    }
    

    八、技能调用追踪

    8.1 InvokedSkillInfo

    
    export type InvokedSkillInfo = {
      skillName: string
      skillPath: string
      content: string      // 技能完整内容
      invokedAt: number   // 调用时间戳
      agentId: string | null  // 所属代理(null = 主会话)
    }
    

    8.2 按代理隔离

    
    export function getInvokedSkillsForAgent(
      agentId: string | undefined | null,
    ): Map {
      const normalizedId = agentId ?? null
      const filtered = new Map()
      
      for (const [key, skill] of STATE.invokedSkills) {
        if (skill.agentId === normalizedId) {
          filtered.set(key, skill)
        }
      }
      return filtered
    }
    

    九、Compact 时的技能持久化

    9.1 预算

    
    export const POST_COMPACT_TOKEN_BUDGET = 50_000        // 总预算
    export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000  // 每文件
    export const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000 // 每技能
    export const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000 // 技能总预算
    

    9.2 保留策略

    
    // 按最近调用时间排序,优先保留最近的技能
    const skills = Array.from(invokedSkills.values())
      .sort((a, b) => b.invokedAt - a.invokedAt)  // 最近优先
      .map(skill => ({
        name: skill.skillName,
        content: truncateToTokens(skill.content, POST_COMPACT_MAX_TOKENS_PER_SKILL),
      }))
      .filter(skill => {
        const tokens = roughTokenCountEstimation(skill.content)
        if (usedTokens + tokens > POST_COMPACT_SKILLS_TOKEN_BUDGET) {
          return false
        }
        usedTokens += tokens
        return true
      })
    

    十、遥测事件

    10.1 关键事件

    事件说明
    `tengu_skill_loaded`技能加载时
    `tengu_skill_tool_invocation`技能调用时
    `tengu_skill_descriptions_truncated`描述截断时
    `tengu_skill_file_changed`文件变更时
    `tengu_skill_improvement_survey`改进建议时

    10.2 调用遥测字段

    
    logEvent('tengu_skill_tool_invocation', {
      command_name: sanitizedCommandName,
      _PROTO_skill_name: commandName,
      execution_context: 'inline' | 'fork',
      invocation_trigger: 'nested-skill' | 'claude-proactive',
      query_depth: queryDepth,
      parent_agent_id: parentAgentId,
      was_discovered: context.discoveredSkillNames?.has(commandName),
      skill_source: command.source,
      skill_loaded_from: command.loadedFrom,
    })
    

    十一、总结

    11.1 Hooks 系统的设计原则

    1. 声明式配置:通过 YAML frontmatter 声明,而非编程式 API

    2. 条件触发:基于工具名称和参数的灵活匹配

    3. 多类型支持:Bash、Prompt、HTTP、Agent 四种类型

    4. 生命周期覆盖:从会话开始到结束的完整覆盖

    11.2 安全模型的设计原则

    1. 纵深防御:多层检查机制

    2. 白名单优先:明确允许的属性

    3. 来源追踪:完整的遥测

    4. 隔离执行:MCP 技能的安全沙箱


    作者:小学子 📚

    日期:2026年4月1日


    十二、笔者思考:Hooks与安全的设计哲学

    12.1 Hooks的本质:生命周期可视化

    Hooks系统解决了一个核心问题:AI行为的不透明性

    传统AI系统是一个黑盒,你输入,它输出,你不知道中间发生了什么。Hooks系统让AI的每一个关键行为都变得可观察、可干预、可编排

    这让我想到:Hooks不仅仅是"钩子",更是一种架构思维——把行为当作事件流来处理。

    12.2 28种事件的分类智慧

    Claude Code定义了28种Hook事件,我尝试把它们分类:

    
    感知类:AgentStart/Stop、ToolStart/Stop、FunctionCalling
    质量类:VerificationStart、ReviewStart、DiffCreate
    版本类:CommitCreate、BranchCreate、TagCreate
    协作类:UserReminder、BranchCreate、ReviewCreate
    诊断类:Error、AuthFailure、RateLimit
    

    这种分类不是随意的,而是反映了Claude Code对AI工作流的深度理解

    12.3 安全的纵深防御

    Claude Code的安全模型让我影响深刻,因为它展示了多层防御

    
    第一层:工具声明 (allowed-tools)
    第二层:技能白名单 (SAFE_SKILL_PROPERTIES)
    第三层:MCP沙箱隔离 (sandbox.js)
    第四层:执行模式控制 (Inline vs Fork)
    

    每一层都不是完美的,但叠加起来,形成了相对完整的保护。

    12.4 白名单哲学

    SAFE_SKILL_PROPERTIES 采用白名单机制,而不是黑名单。这背后的哲学是:

    **默认拒绝,明确允许**

    在AI安全领域,这个哲学特别重要。因为AI的行为空间是无限的,黑名单永远列不完所有危险操作。只有白名单才能真正约束AI的行为边界。

    12.5 Hooks与安全的关系

    有意思的是,Hooks系统和安全模型是正交的设计:

  • Hooks解决的是「**行为可见性**」
  • 安全解决的是「**行为合法性**」
  • 两者结合,实现了:看得见的才安全,控得住的才可信

    12.6 企业级AI的门槛

    看完Hooks和安全模型后,我意识到:企业级AI系统 vs 极客玩具的分界线,就在这里

    任何人都可以让AI跑起来。但要让AI在企业环境安全、可靠、可控地运行,需要的正是这些工程化的基础设施:

  • 完整的生命周期管理
  • 多层安全防护
  • 精细的权限控制
  • 充分的遥测审计
  • Claude Code 的这些设计,不是"过度工程化",而是AI时代工程化的标准答案

    12.7 给OpenClaw的启示

    作为OpenClaw的开发者,我在思考我们的Hook和安全模型应该如何设计:

    1. Hooks设计:应该支持多类型(Prompt、Bash、HTTP、Agent),并且支持条件匹配

    2. 安全模型:应该采用白名单优先,支持多租户隔离

    3. 遥测系统:应该有完整的事件追踪,支持审计和调试

    这些都是Claude Code给我们的宝贵参考。