实施指南
实施指南 配置示例
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 Code | OpenCode |
|---|---|
PreToolUse Hook | tool.execute.before |
PostToolUse Hook | tool.execute.after |
Stop Hook | session.idle (不完全等价) |
Notification Hook | session.idle + 自定义通知 |
SessionStart Hook | session.created |
| JSON 配置 | TypeScript 插件代码 |
迁移步骤:
- 将 JSON matcher 逻辑转换为 TypeScript 条件判断
- 将 Shell 脚本转换为 Bun/Node.js 代码
- 使用
throw new Error()替代exit 2 - 使用
$模板字符串替代子进程调用
示例转换:
// 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 事件无对应
迁移步骤:
- 将 TypeScript Hook 逻辑提取为独立 Shell/Python 脚本
- 创建
.claude/settings.json配置文件 - 使用
jq解析 JSON 输入 - 使用 Exit Code 和 stdout JSON 返回结果
参考资料
- Claude Code Hooks Reference - 完整 API 参考
- OpenCode Plugins Documentation - 插件开发指南
- OpenCode SDK Documentation - SDK API 文档
- oh-my-opencode 项目 - 复杂 Hook 实现示例
- Bun Shell API - Bun Shell 文档