Logo
热心市民王先生

实施指南

实施指南 配置示例

Hook 机制的具体配置与代码实现指南

Claude Code Hook 配置

环境配置

步骤一:创建配置文件

Claude Code 的 Hook 配置存储在 settings.json 文件中,支持多个位置:

# 用户全局配置(推荐用于个人偏好)
~/.claude/settings.json

# 项目配置(推荐提交到 Git)
.claude/settings.json

# 项目本地配置(敏感信息,不提交)
.claude/settings.local.json

步骤二:配置目录结构

mkdir -p .claude/hooks
chmod +x .claude/hooks/*.sh  # 确保脚本可执行

步骤三:安装依赖工具

Hook 脚本通常需要 jq 来解析 JSON 输入:

# macOS
brew install jq

# Ubuntu/Debian
sudo apt-get install jq

# Windows (via Chocolatey)
choco install jq

代码示例

示例一:完整的 PreToolUse Hook 脚本

#!/bin/bash
# .claude/hooks/pre-tool-validate.sh
# 用途:在工具执行前进行验证

set -e

# 读取 stdin 的 JSON 输入
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input')

log_error() {
  echo "$1" >&2
}

# Bash 命令验证
if [ "$TOOL_NAME" = "Bash" ]; then
  COMMAND=$(echo "$TOOL_INPUT" | jq -r '.command')
  
  # 检查危险命令
  if echo "$COMMAND" | grep -qE '(rm\s+-rf|DROP\s+TABLE|truncate|mkfs)'; then
    jq -n '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "检测到危险命令模式,已拦截"
      }
    }'
    exit 0
  fi
  
  # 检查生产环境命令
  if echo "$COMMAND" | grep -qE 'production|prod|live'; then
    jq -n '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "ask",
        permissionDecisionReason: "检测到可能影响生产环境的命令"
      }
    }'
    exit 0
  fi
fi

# 文件写入验证
if [ "$TOOL_NAME" = "Write" ] || [ "$TOOL_NAME" = "Edit" ]; then
  FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path')
  
  # 保护敏感文件
  PROTECTED_FILES=(".env" "credentials" "secrets" "private_key" "id_rsa")
  for pattern in "${PROTECTED_FILES[@]}"; do
    if [[ "$FILE_PATH" == *"$pattern"* ]]; then
      jq -n --arg reason "禁止修改敏感文件: $FILE_PATH" '{
        hookSpecificOutput: {
          hookEventName: "PreToolUse",
          permissionDecision: "deny",
          permissionDecisionReason: $reason
        }
      }'
      exit 0
    fi
  done
fi

# 允许执行
exit 0

示例二:PostToolUse 自动格式化

#!/bin/bash
# .claude/hooks/post-format.sh
# 用途:文件编辑后自动运行格式化

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')

if [ "$TOOL_NAME" = "Write" ] || [ "$TOOL_NAME" = "Edit" ]; then
  FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
  
  # 根据文件扩展名选择格式化工具
  EXT="${FILE_PATH##*.}"
  
  case "$EXT" in
    ts|tsx|js|jsx|json|md)
      npx prettier --write "$FILE_PATH" 2>/dev/null || true
      ;;
    py)
      python -m black "$FILE_PATH" 2>/dev/null || true
      ;;
    go)
      gofmt -w "$FILE_PATH" 2>/dev/null || true
      ;;
  esac
fi

exit 0

示例三:Notification Hook 桌面通知

#!/bin/bash
# .claude/hooks/notification.sh
# 用途:发送桌面通知

INPUT=$(cat)
NOTIFICATION_TYPE=$(echo "$INPUT" | jq -r '.notification_type')

# macOS 通知
if [[ "$OSTYPE" == "darwin"* ]]; then
  osascript -e "display notification \"Claude Code 需要您的注意\" with title \"$NOTIFICATION_TYPE\""
fi

# Linux 通知
if [[ "$OSTYPE" == "linux"* ]]; then
  notify-send "Claude Code" "需要您的注意: $NOTIFICATION_TYPE"
fi

exit 0

配置文件示例:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-tool-validate.sh",
            "timeout": 30
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-format.sh",
            "timeout": 60
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "idle_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/notification.sh"
          }
        ]
      }
    ]
  }
}

常见问题与解决

问题一:Hook 不触发

# 检查配置是否正确加载
claude /hooks

# 确认 matcher 语法正确
# matcher 是正则表达式,区分大小写

# 检查脚本权限
chmod +x .claude/hooks/*.sh

# 检查脚本语法
bash -n .claude/hooks/your-hook.sh

问题二:JSON 解析失败

这通常是因为 shell 配置文件(.bashrc / .zshrc)在启动时输出了文本。解决方案:

# 在 .zshrc 或 .bashrc 中包装输出
if [[ $- == *i* ]]; then
  echo "Shell ready"  # 只在交互模式输出
fi

问题三:超时问题

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "your-script.sh",
            "timeout": 120  // 增加超时时间(秒)
          }
        ]
      }
    ]
  }
}

OpenCode Hook 配置

环境配置

步骤一:创建插件目录

# 全局插件
mkdir -p ~/.config/opencode/plugins

# 项目插件
mkdir -p .opencode/plugins

步骤二:安装依赖(如需要)

# 在项目根目录创建 package.json
cd .opencode
cat > package.json << 'EOF'
{
  "dependencies": {
    "@opencode-ai/plugin": "^1.1.0",
    "zod": "^3.0.0"
  }
}
EOF

# OpenCode 会自动使用 Bun 安装依赖

步骤三:创建插件文件

# 创建插件入口文件
touch .opencode/plugins/my-hooks.ts

代码示例

示例一:完整的 OpenCode 插件

// .opencode/plugins/my-hooks.ts
import type { Plugin, Tool } from "@opencode-ai/plugin"
import { z } from "zod"

export const MyHooksPlugin: Plugin = async (ctx) => {
  const { project, client, $, directory, worktree } = ctx

  // ====== 工具执行前验证 ======
  const validateToolExecution = async (input: any, output: any) => {
    // Bash 命令验证
    if (input.tool === "bash") {
      const command = output.args.command as string
      
      // 危险命令检查
      const dangerousPatterns = [
        /rm\s+-rf\s+\//,
        /DROP\s+TABLE/i,
        /truncate\s+table/i,
        /:\(\)\{\s*:\|:&\s*\};:/
      ]
      
      for (const pattern of dangerousPatterns) {
        if (pattern.test(command)) {
          throw new Error(`危险命令已拦截: ${command}`)
        }
      }
    }
    
    // 文件操作保护
    if (["read", "write", "edit"].includes(input.tool)) {
      const filePath = output.args.filePath as string
      const protectedFiles = [".env", "credentials.json", "secrets.yaml"]
      
      for (const protected of protectedFiles) {
        if (filePath.includes(protected)) {
          throw new Error(`禁止访问敏感文件: ${protected}`)
        }
      }
    }
  }

  // ====== 工具执行后处理 ======
  const postToolHandler = async (input: any, output: any) => {
    // 记录工具调用
    await client.app.log({
      body: {
        service: "my-hooks",
        level: "debug",
        message: `Tool executed: ${input.tool}`,
        extra: { input, output }
      }
    })
  }

  // ====== 会话通知 ======
  const sessionIdleHandler = async ({ event }: any) => {
    // macOS 通知
    try {
      await $`osascript -e 'display notification "OpenCode 会话已完成" with title "OpenCode"'`
    } catch {}
  }

  // ====== 返回 Hook 配置 ======
  return {
    "tool.execute.before": validateToolExecution,
    "tool.execute.after": postToolHandler,
    "session.idle": sessionIdleHandler,
    
    // ====== 自定义工具 ======
    tool: {
      run_tests: {
        description: "运行项目测试套件",
        parameters: z.object({
          pattern: z.string().optional().describe("测试文件匹配模式")
        }),
        execute: async (args, context) => {
          const pattern = args.pattern || "test"
          const result = await $`bun test ${pattern}`
          return result.stdout
        }
      } as Tool
    }
  }
}

示例二:上下文注入 Hook

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

export const ContextInjectorPlugin: Plugin = async (ctx) => {
  const { directory, project } = ctx
  
  return {
    // 会话压缩时注入持久化上下文
    "experimental.session.compacting": async (input, output) => {
      // 获取当前 TODO 状态
      let todoStatus = "无活动任务"
      try {
        const todoContent = await Bun.file(`${directory}/.opencode/todos.json`).text()
        const todos = JSON.parse(todoContent)
        const pending = todos.filter((t: any) => t.status !== "completed")
        if (pending.length > 0) {
          todoStatus = pending.map((t: any) => `- ${t.content}`).join("\n")
        }
      } catch {}
      
      // 注入上下文
      output.context.push(`
## 会话持久化上下文

### 当前项目
- 名称: ${project.name}
- 目录: ${directory}

### 未完成任务
${todoStatus}

### 重要决策
[在此记录关键架构决策]

### 注意事项
- 使用 Bun 而非 npm
- 测试覆盖率要求 > 80%
- 提交前运行 bun lint
      `)
    }
  }
}

示例三:环境变量管理

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

export const EnvManagerPlugin: Plugin = async (ctx) => {
  const { directory } = ctx
  
  return {
    "shell.env": async (input, output) => {
      // 注入项目环境变量
      output.env.PROJECT_ROOT = directory
      output.env.NODE_ENV = process.env.NODE_ENV || "development"
      
      // 从 .env.local 加载
      try {
        const envPath = `${directory}/.env.local`
        const envContent = await Bun.file(envPath).text()
        
        envContent.split("\n").forEach(line => {
          const trimmed = line.trim()
          if (trimmed && !trimmed.startsWith("#")) {
            const [key, ...values] = trimmed.split("=")
            if (key) {
              output.env[key.trim()] = values.join("=").trim().replace(/^["']|["']$/g, "")
            }
          }
        })
      } catch {}
    }
  }
}

常见问题与解决

问题一:插件未加载

# 检查插件文件位置
ls -la .opencode/plugins/
ls -la ~/.config/opencode/plugins/

# 确认导出名称正确
# 必须使用 export const PluginName: Plugin = async ...

问题二:依赖找不到

# 确保有 package.json
cat .opencode/package.json

# 手动安装依赖
cd .opencode && bun install

问题三:Hook 不生效

// 确认 Hook 函数签名正确
"tool.execute.before": async (input, output) => {
  // input: { tool: string, ... }
  // output: { args: { ... }, ... }
  
  // 阻断方式:抛出异常
  throw new Error("原因")
  
  // 修改方式:修改 output 对象
  output.args.param = "新值"
}

迁移指南

从 Claude Code 迁移到 OpenCode

概念映射:

Claude CodeOpenCode
PreToolUse Hooktool.execute.before
PostToolUse Hooktool.execute.after
Stop Hooksession.idle (不完全等价)
Notification Hooksession.idle + 自定义通知
SessionStart Hooksession.created
JSON 配置TypeScript 插件代码

迁移步骤:

  1. 将 JSON matcher 逻辑转换为 TypeScript 条件判断
  2. 将 Shell 脚本转换为 Bun/Node.js 代码
  3. 使用 throw new Error() 替代 exit 2
  4. 使用 $ 模板字符串替代子进程调用

示例转换:

// Claude Code JSON:
// {
//   "hooks": {
//     "PreToolUse": [
//       { "matcher": "Bash", "hooks": [{ "type": "command", "command": "validate.sh" }] }
//     ]
//   }
// }

// OpenCode TypeScript:
export const MigratedPlugin: Plugin = async () => {
  return {
    "tool.execute.before": async (input, output) => {
      if (input.tool === "bash") {
        const command = output.args.command
        if (/rm\s+-rf/.test(command)) {
          throw new Error("危险命令已拦截")
        }
      }
    }
  }
}

从 OpenCode 迁移到 Claude Code

限制说明:

  • Claude Code 不支持自定义工具注册,相关功能需移除
  • Claude Code 不支持同进程的 Hook,需转换为外部脚本
  • Claude Code 的 Hook 类型有限,部分 OpenCode 事件无对应

迁移步骤:

  1. 将 TypeScript Hook 逻辑提取为独立 Shell/Python 脚本
  2. 创建 .claude/settings.json 配置文件
  3. 使用 jq 解析 JSON 输入
  4. 使用 Exit Code 和 stdout JSON 返回结果

参考资料