Logo
热心市民王先生

解决方案设计

解决方案 最佳实践

设计 Hook 使用场景的最佳实践方案

Claude Code Hook 最佳实践

原生 Hook 使用方案

Claude Code 的 Hook 设计强调开箱即用低门槛配置。以下是几种典型场景的最佳实践:

场景一:自动代码格式化

最常见的 Hook 用例是在文件编辑后自动运行格式化工具。这消除了手动运行 Prettier 的繁琐:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

场景二:危险命令拦截

PreToolUse 中拦截危险的 Bash 命令,防止误操作:

#!/bin/bash
# .claude/hooks/block-dangerous.sh
COMMAND=$(jq -r '.tool_input.command')

# 危险命令模式
DANGEROUS_PATTERNS=("rm -rf" "rm -rf /" "DROP TABLE" "truncate" ":(){ :|:& };:")

for pattern in "${DANGEROUS_PATTERNS[@]}"; do
  if [[ "$COMMAND" == *"$pattern"* ]]; then
    jq -n '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "危险命令已拦截: 包含 '"$pattern"'"
      }
    }'
    exit 0
  fi
done

exit 0  # 允许执行

场景三:Ralph-Loop 自主循环

利用 Stop Hook 实现 Claude 的自主循环,在任务未完成时自动继续:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/stop-hook.sh"
          }
        ]
      }
    ]
  }
}
#!/bin/bash
# stop-hook.sh
INPUT=$(cat)

# 检查是否已经触发过
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0  # 允许停止
fi

# 检查 TODO 是否全部完成
TODO_STATUS=$(cat .opencode/.boulder-state 2>/dev/null | jq -r '.all_completed')

if [ "$TODO_STATUS" != "true" ]; then
  # 返回继续指令
  jq -n '{
    decision: "block",
    reason: "任务尚未完成,请继续工作。检查未完成的 TODO 项目。"
  }'
fi

配置示例

完整的多功能 Hook 配置:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo '提醒: 使用 Bun 而非 npm。运行 bun test 后再提交。'"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate-bash.sh"
          }
        ]
      },
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs -I {} npx prettier --write {}"
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "idle_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude 需要您的输入\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

OpenCode Hook 最佳实践

原生 Hook 使用方案

OpenCode 的 Hook 作为插件 API 的一部分,更适合构建复杂功能而非简单自动化:

场景一:敏感文件保护

// .opencode/plugins/env-protection.ts
import type { Plugin } from "@opencode-ai/plugin"

export const EnvProtection: Plugin = async () => {
  return {
    "tool.execute.before": async (input, output) => {
      // 阻止读取 .env 文件
      if (input.tool === "read" && output.args.filePath.includes(".env")) {
        throw new Error("禁止读取 .env 文件,这是安全策略")
      }
      
      // 阻止写入敏感路径
      if (input.tool === "write") {
        const protectedPaths = [".env", "credentials.json", "secrets.yaml"]
        for (const protected of protectedPaths) {
          if (output.args.filePath.includes(protected)) {
            throw new Error(`禁止写入敏感文件: ${protected}`)
          }
        }
      }
    }
  }
}

场景二:环境变量注入

// .opencode/plugins/inject-env.ts
import type { Plugin } from "@opencode-ai/plugin"

export const InjectEnv: Plugin = async () => {
  return {
    "shell.env": async (input, output) => {
      // 自动注入项目相关环境变量
      output.env.PROJECT_ROOT = input.cwd
      output.env.NODE_ENV = process.env.NODE_ENV || "development"
      
      // 从 .env 文件加载(如果有)
      const envPath = `${input.cwd}/.env`
      try {
        const envContent = await Bun.file(envPath).text()
        envContent.split("\n").forEach(line => {
          const [key, ...values] = line.split("=")
          if (key && values.length) {
            output.env[key.trim()] = values.join("=").trim()
          }
        })
      } catch {}
    }
  }
}

场景三:自定义工具注册

// .opencode/plugins/custom-tools.ts
import { type Plugin, tool } from "@opencode-ai/plugin"

export const CustomTools: Plugin = async (ctx) => {
  return {
    tool: {
      // 注册 Jira 集成工具
      jira_search: tool({
        description: "搜索 Jira 工单",
        args: {
          query: tool.schema.string().describe("搜索关键词")
        },
        async execute(args, context) {
          const response = await fetch(
            `https://your-company.atlassian.net/rest/api/3/search?jql=text~"${args.query}"`,
            {
              headers: {
                Authorization: `Bearer ${process.env.JIRA_TOKEN}`
              }
            }
          )
          const data = await response.json()
          return JSON.stringify(data.issues, null, 2)
        }
      }),
      
      // 注册数据库查询工具
      db_query: tool({
        description: "执行只读数据库查询",
        args: {
          sql: tool.schema.string().describe("SELECT 查询语句")
        },
        async execute(args, context) {
          if (!args.sql.toUpperCase().startsWith("SELECT")) {
            throw new Error("只允许 SELECT 查询")
          }
          // 实现数据库查询逻辑
          return "查询结果..."
        }
      })
    }
  }
}

配置示例

oh-my-opencode 风格的复杂插件结构:

// .opencode/plugins/my-plugin/index.ts
import type { Plugin } from "@opencode-ai/plugin"
import { createTodoEnforcerHook } from "./hooks/todo-enforcer"
import { createContextInjectorHook } from "./hooks/context-injector"
import { createNotificationHook } from "./hooks/notification"

export const MyPlugin: Plugin = async (ctx) => {
  const { project, client, $, directory, worktree } = ctx
  
  // 初始化日志
  await client.app.log({
    body: {
      service: "my-plugin",
      level: "info",
      message: "插件初始化中..."
    }
  })
  
  return {
    // 会话空闲时发送通知
    "session.idle": createNotificationHook(ctx),
    
    // 工具执行前检查
    "tool.execute.before": async (input, output) => {
      // 实现 TODO 强制完成检查
      // 实现上下文窗口监控
      // 实现敏感操作拦截
    },
    
    // 会话压缩时注入上下文
    "experimental.session.compacting": async (input, output) => {
      output.context.push(`
## 持久化上下文
- 当前任务: ${project.name}
- 工作目录: ${directory}
- 重要决策: [记录关键决策]
      `)
    },
    
    // 注册自定义工具
    tool: {
      my_custom_tool: tool({
        description: "自定义工具描述",
        args: {
          param: tool.schema.string()
        },
        async execute(args, context) {
          return "工具执行结果"
        }
      })
    }
  }
}

架构对比图

数据流设计

graph TB
    subgraph "Claude Code Hook 数据流"
        CC1[Claude Code 主进程] -->|事件触发| CC2[Hook 匹配器]
        CC2 -->|匹配成功| CC3[启动子进程]
        CC3 -->|stdin: JSON 输入| CC4[Hook 脚本]
        CC4 -->|stdout: JSON 输出| CC3
        CC3 -->|Exit Code| CC1
        CC1 -->|处理结果| CC5[继续/阻断/询问]
    end
    
    subgraph "OpenCode Hook 数据流"
        OC1[OpenCode Runtime] -->|事件触发| OC2[Plugin Hook 函数]
        OC2 -->|直接调用| OC3[Hook Handler]
        OC3 -->|修改 output 对象| OC2
        OC3 -->|抛出异常| OC2
        OC2 -->|返回结果| OC1
        OC1 -->|继续执行/阻断| OC4[下一步操作]
    end

执行流程对比

阶段Claude CodeOpenCode
配置加载启动时读取 JSON,支持热重载插件加载时执行函数
事件触发按事件类型查找匹配的 Hook直接调用已注册的 Hook 函数
参数传递JSON 序列化到 stdin直接传递 JavaScript 对象
结果返回解析 stdout JSON / Exit Code函数返回值 / 对象修改
错误处理非 0 Exit Code = 错误异常 = 错误
超时控制内置 timeout 字段需自行实现

性能对比:

指标Claude CodeOpenCode
Hook 调用延迟~50-200ms(进程启动)~1-5ms(函数调用)
内存隔离完全隔离共享运行时
崩溃影响不影响主进程可能导致主进程崩溃
跨语言支持任意语言脚本JavaScript/TypeScript

参考资料