产品动态
conversationId 判断是否为新会话sourcestartup - 首次启动(目前仅支持此值){"session_id": "abc123","transcript_path": "/path/to/transcript.txt","cwd": "/project/path","hook_event_name": "SessionStart","source": "startup"}
{"continue": true,"hookSpecificOutput": {"hookEventName": "SessionStart","additionalContext": "项目使用 TypeScript + React,请优先使用函数式组件"}}
{"hooks": {"SessionStart": [{"matcher": "startup","hooks": [{"type": "command","command": "/path/to/session_start.py","timeout": 30}]}]}}
reasonother - 会话结束(目前仅支持此值,包括切换会话、删除会话、清空会话等场景){"session_id": "abc123","transcript_path": "/path/to/transcript.txt","cwd": "/project/path","hook_event_name": "SessionEnd","reason": "other"}
{"continue": true,"systemMessage": "会话已清理,临时文件已删除"}
tool_nameexecute_command, write_to_file, read_filewrite_to_file|replace_in_file* 或空字符串{"session_id": "abc123","transcript_path": "/path/to/transcript.txt","cwd": "/project/path","hook_event_name": "PreToolUse","tool_name": "execute_command","tool_input": {"command": "npm install","requires_approval": false}}
{"continue": true,"hookSpecificOutput": {"hookEventName": "PreToolUse","permissionDecision": "allow"}}
{"continue": true,"hookSpecificOutput": {"hookEventName": "PreToolUse","permissionDecision": "allow","permissionDecisionReason": "已添加 --legacy-peer-deps 参数","modifiedInput": {"command": "npm install --legacy-peer-deps","requires_approval": false}}}
{"continue": false,"hookSpecificOutput": {"hookEventName": "PreToolUse","permissionDecision": "deny","permissionDecisionReason": "检测到危险命令: rm -rf /"}}
{"continue": true,"hookSpecificOutput": {"hookEventName": "PreToolUse","permissionDecision": "ask","permissionDecisionReason": "检测到 git push --force,是否继续?"}}
tool_name{"session_id": "abc123","transcript_path": "/path/to/transcript.txt","cwd": "/project/path","hook_event_name": "PostToolUse","tool_name": "execute_command","tool_input": {"command": "npm test"},"tool_response": {"exitCode": 0,"stdout": "All tests passed","stderr": ""}}
{"continue": true,"hookSpecificOutput": {"hookEventName": "PostToolUse","additionalContext": "测试已通过,可以继续开发"}}
{"session_id": "abc123","transcript_path": "/path/to/transcript.txt","cwd": "/project/path","hook_event_name": "UserPromptSubmit","prompt": "帮我实现一个登录功能"}
{"continue": true,"hookSpecificOutput": {"hookEventName": "UserPromptSubmit","additionalContext": "提示:项目已集成 JWT 认证库,建议使用"}}
{"continue": false,"stopReason": "输入包含敏感信息,已阻止"}
{"session_id": "abc123","transcript_path": "/path/to/transcript.txt","cwd": "/project/path","hook_event_name": "Stop","stop_hook_active": false}
{"continue": false,"stopReason": "请验证代码是否通过了单元测试"}
triggermanual - 用户手动触发 /summarizeauto - 自动压缩{"session_id": "abc123","transcript_path": "/path/to/transcript.txt","cwd": "/project/path","hook_event_name": "PreCompact","trigger": "auto","custom_instructions": "保留所有 API 设计相关的讨论"}
{"continue": true,"hookSpecificOutput": {"hookEventName": "PreCompact","additionalContext": "重要:保留数据库表结构设计"}}
{"session_id": "会话 ID","transcript_path": "对话记录文件路径","cwd": "当前工作目录","hook_event_name": "事件名称"}
SessionStart: sourceSessionEnd: reasonPreToolUse/PostToolUse: tool_name, tool_input, tool_responseUserPromptSubmit: promptPreCompact: trigger, custom_instructionsStop: stop_hook_active{"continue": true,"suppressOutput": false,"systemMessage": "可选的系统消息","stopReason": "阻止原因(当 continue=false 时)","hookSpecificOutput": {"hookEventName": "事件名称","permissionDecision": "allow|deny|ask","permissionDecisionReason": "决策原因","modifiedInput": {},"additionalContext": "额外上下文"}}
continue: 是否允许操作继续(false 表示阻止)suppressOutput: 是否隐藏 stdout 输出systemMessage: 显示给用户的系统消息stopReason: 阻止原因hookSpecificOutput: 事件特定的输出数据退出码 | 含义 | 行为 |
0 | 成功执行 | 允许操作继续,stdout 可能被处理 |
1 | 非阻塞错误 | 显示 stderr 作为警告,允许继续 |
2 | 阻塞错误 | 阻止操作,stderr 传递给 Agent/模型 |
其他 | 非阻塞错误 | 同退出码 1 |
CLAUDE_PROJECT_DIR: 项目根目录(兼容 Claude Code)CODEBUDDY_PROJECT_DIR: 项目根目录(CodeBuddy 特定)<workspace>/.codebuddy/settings.json~/.codebuddy/settings.json{"hooks": {"PreToolUse": [{"matcher": "execute_command","hooks": [{"type": "command","command": "/absolute/path/to/script.py","timeout": 10}]},{"matcher": "write_to_file|replace_in_file","hooks": [{"type": "command","command": "/path/to/backup_script.sh","timeout": 20}]}],"SessionStart": [{"matcher": "startup","hooks": [{"type": "command","command": "/path/to/init.py","timeout": 30}]}]}}
"" 或 "*": 匹配所有"execute_command""write_to_file|replace_in_file""read.*|search.*"PreToolUse/PostToolUse: 匹配 tool_nameSessionStart: 匹配 sourceSessionEnd: 匹配 reasonPreCompact: 匹配 triggerUserPromptSubmit/Stop: 不使用 matcher"$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/script.py""python3 /path/to/script.py"60 秒rm -rf 命令validate_command.py):#!/usr/bin/env python3import jsonimport sysDANGEROUS_COMMANDS = ['rm -rf /', 'dd if=/dev/zero', 'mkfs']def main():input_data = json.loads(sys.stdin.read())if input_data.get('tool_name') != 'execute_command':print(json.dumps({"continue": True}))return 0command = input_data.get('tool_input', {}).get('command', '')for dangerous in DANGEROUS_COMMANDS:if dangerous in command:output = {"continue": False,"hookSpecificOutput": {"hookEventName": "PreToolUse","permissionDecision": "deny","permissionDecisionReason": f"检测到危险命令: {dangerous}"}}print(json.dumps(output, ensure_ascii=False))return 0print(json.dumps({"continue": True}))return 0if __name__ == "__main__":sys.exit(main())
{"hooks": {"PreToolUse": [{"matcher": "execute_command","hooks": [{"type": "command","command": "/path/to/validate_command.py","timeout": 10}]}]}}
npm install 添加 --legacy-peer-deps 参数modify_npm.py):#!/usr/bin/env python3import jsonimport sysimport redef main():input_data = json.loads(sys.stdin.read())if input_data.get('tool_name') != 'execute_command':print(json.dumps({"continue": True}))return 0tool_input = input_data.get('tool_input', {})command = tool_input.get('command', '')# 检查是否是 npm installif re.match(r'^npm\\s+(i|install)\\b', command.strip()):# 如果没有 --legacy-peer-deps,添加它if '--legacy-peer-deps' not in command:modified_command = command.strip() + ' --legacy-peer-deps'output = {"continue": True,"hookSpecificOutput": {"hookEventName": "PreToolUse","permissionDecision": "allow","permissionDecisionReason": "已自动添加 --legacy-peer-deps 参数","modifiedInput": {"command": modified_command,"requires_approval": tool_input.get('requires_approval', False)}}}print(json.dumps(output, ensure_ascii=False))return 0print(json.dumps({"continue": True}))return 0if __name__ == "__main__":sys.exit(main())
backup_files.py):#!/usr/bin/env python3import jsonimport sysimport osimport shutilfrom datetime import datetimedef main():input_data = json.loads(sys.stdin.read())tool_name = input_data.get('tool_name', '')# 只处理文件写入工具if tool_name not in ['write_to_file', 'replace_in_file']:print(json.dumps({"continue": True}))return 0tool_input = input_data.get('tool_input', {})file_path = tool_input.get('filePath')if not file_path or not os.path.exists(file_path):print(json.dumps({"continue": True}))return 0# 创建备份目录project_dir = os.environ.get('CODEBUDDY_PROJECT_DIR', '')backup_dir = os.path.join(project_dir, '.codebuddy', 'backups')os.makedirs(backup_dir, exist_ok=True)# 生成备份文件名timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')backup_name = f"{os.path.basename(file_path)}.{timestamp}.bak"backup_path = os.path.join(backup_dir, backup_name)# 创建备份shutil.copy2(file_path, backup_path)output = {"continue": True,"systemMessage": f"已备份至: {backup_path}"}print(json.dumps(output, ensure_ascii=False))return 0if __name__ == "__main__":sys.exit(main())
{"hooks": {"PreToolUse": [{"matcher": "write_to_file|replace_in_file","hooks": [{"type": "command","command": "/path/to/backup_files.py","timeout": 15}]}]}}
session_start.py):#!/usr/bin/env python3import jsonimport sysimport osdef main():input_data = json.loads(sys.stdin.read())project_dir = os.environ.get('CODEBUDDY_PROJECT_DIR', '')# 读取项目配置config_file = os.path.join(project_dir, '.codebuddy', 'project.json')project_info = ""if os.path.exists(config_file):with open(config_file, 'r') as f:config = json.load(f)project_info = f"""项目名称: {config.get('name', 'Unknown')}技术栈: {', '.join(config.get('tech_stack', []))}编码规范: {config.get('coding_standard', 'Standard')}"""output = {"continue": True,"hookSpecificOutput": {"hookEventName": "SessionStart","additionalContext": f"""会话已启动!项目目录: {project_dir}启动源: {input_data.get('source', 'unknown')}{project_info}"""}}print(json.dumps(output, ensure_ascii=False))return 0if __name__ == "__main__":sys.exit(main())
save_context.py):#!/usr/bin/env python3import jsonimport sysimport osimport shutilfrom datetime import datetimedef main():input_data = json.loads(sys.stdin.read())# 只处理自动压缩if input_data.get('trigger') != 'auto':print(json.dumps({"continue": True}))return 0project_dir = os.environ.get('CODEBUDDY_PROJECT_DIR', '')transcript_path = input_data.get('transcript_path', '')if not transcript_path or not os.path.exists(transcript_path):print(json.dumps({"continue": True}))return 0# 创建保存目录save_dir = os.path.join(project_dir, '.codebuddy', 'context_history')os.makedirs(save_dir, exist_ok=True)# 保存对话历史timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')save_path = os.path.join(save_dir, f'transcript_{timestamp}.txt')shutil.copy2(transcript_path, save_path)output = {"continue": True,"systemMessage": f"上下文已保存至: {save_path}"}print(json.dumps(output, ensure_ascii=False))return 0if __name__ == "__main__":sys.exit(main())
{"hooks": {"PreCompact": [{"matcher": "auto","hooks": [{"type": "command","command": "/path/to/save_context.py","timeout": 20}]}]}}
mkdir -p ~/.codebuddycat > ~/.codebuddy/settings.json << 'EOF'{"hooks": {"SessionStart": [{"matcher": "startup","hooks": [{"type": "command","command": "/usr/bin/env python3 -c \\"import json,sys; print(json.dumps({'continue': True, 'hookSpecificOutput': {'hookEventName': 'SessionStart', 'additionalContext': 'Hook 配置成功!'}}))\\"","timeout": 5}]}]}}EOF
# 创建 Hook 脚本目录mkdir -p ~/.codebuddy/hooks# 创建测试脚本cat > ~/.codebuddy/hooks/my_first_hook.py << 'EOF'#!/usr/bin/env python3import jsonimport sysdef main():input_data = json.loads(sys.stdin.read())output = {"continue": True,"hookSpecificOutput": {"hookEventName": "SessionStart","additionalContext": f"欢迎使用 Agent-Craft!当前项目: {input_data.get('cwd', 'unknown')}"}}print(json.dumps(output, ensure_ascii=False))return 0if __name__ == "__main__":sys.exit(main())EOF# 添加执行权限chmod +x ~/.codebuddy/hooks/my_first_hook.py
{"hooks": {"SessionStart": [{"matcher": "startup","hooks": [{"type": "command","command": "/Users/YOUR_USERNAME/.codebuddy/hooks/my_first_hook.py","timeout": 10}]}]}}
import sysdef debug_log(message):"""写入调试日志而不影响 stdout"""with open('/tmp/hook_debug.log', 'a') as f:f.write(f"{message}\\n")# 在 Hook 脚本中使用debug_log(f"Received input: {json.dumps(input_data)}")
# 测试脚本输出的 JSON 是否有效echo '{"hook_event_name":"SessionStart"}' | python3 your_hook.py | jq .
# 实时查看 Hook 日志tail -f ~/.codebuddy/logs/agent-craft.log | grep -i hook
import os# 在 Hook 中获取项目目录project_dir = os.environ.get('CODEBUDDY_PROJECT_DIR', '')claude_dir = os.environ.get('CLAUDE_PROJECT_DIR', '') # 兼容 Claude Code
ALLOWED_COMMANDS = ['npm install','npm test','git status','git diff']def is_allowed(command):return any(command.startswith(allowed) for allowed in ALLOWED_COMMANDS)
def enhance_command(command):"""自动添加常用参数"""enhancements = {'npm install': ' --legacy-peer-deps','git push': ' --dry-run', # 安全模式}for prefix, suffix in enhancements.items():if command.startswith(prefix) and suffix not in command:return command + suffixreturn command
def should_block(input_data):"""根据多个条件判断是否阻止"""tool_name = input_data.get('tool_name')tool_input = input_data.get('tool_input', {})# 规则1: 阻止删除重要文件if tool_name == 'delete_files':file_path = tool_input.get('target_file', '')if any(important in file_path for important in ['.git', 'package.json']):return True, "不能删除重要文件"# 规则2: 阻止危险命令if tool_name == 'execute_command':command = tool_input.get('command', '')if 'rm -rf /' in command or 'dd if=' in command:return True, "检测到危险命令"return False, None
{"hooks": {"SessionStart": [{"matcher": "startup","hooks": [{"type": "command","command": "node ~/.codebuddy/hooks/nodejs-init.js","timeout": 15}]}],"PreToolUse": [{"matcher": "execute_command","hooks": [{"type": "command","command": "python3 ~/.codebuddy/hooks/npm-safety-check.py","timeout": 5}]}]}}
{"hooks": {"SessionStart": [{"matcher": "startup","hooks": [{"type": "command","command": "python3 ~/.codebuddy/hooks/python-env-check.py","timeout": 10}]}],"PostToolUse": [{"matcher": "write_to_file|replace_in_file","hooks": [{"type": "command","command": "python3 ~/.codebuddy/hooks/python-lint.py","timeout": 20}]}]}}
sys.stderr 输出调试信息,不要污染 stdoutecho '{"hook_event_name":"PreToolUse","tool_name":"execute_command","tool_input":{"command":"npm install"}}' | \\python3 /path/to/your_hook.py
# 在 Hook 脚本中输出调试信息import syssys.stderr.write(f"[DEBUG] Processing command: {command}\\n")sys.stderr.flush()
<workspace>/.codebuddy/ 下,随项目版本控制~/.codebuddy/ 下,跨项目复用# 不好的做法:每次都加载大文件def main():with open('huge_config.json', 'r') as f:config = json.load(f) # 每次都读取# ... 处理逻辑# 好的做法:缓存配置CONFIG_CACHE = Nonedef get_config():global CONFIG_CACHEif CONFIG_CACHE is None:with open('huge_config.json', 'r') as f:CONFIG_CACHE = json.load(f)return CONFIG_CACHE
{"hooks": {"PreToolUse": [{"matcher": "execute_command","hooks": [{"type": "command","command": "/path/to/fast_check.sh","timeout": 3}]},{"matcher": ".*","hooks": [{"type": "command","command": "/path/to/general_check.py","timeout": 10}]}]}}
import fcntldef safe_append_log(message):"""线程安全的日志写入"""with open('/tmp/hook.log', 'a') as f:fcntl.flock(f.fileno(), fcntl.LOCK_EX)f.write(message + '\\n')fcntl.flock(f.fileno(), fcntl.LOCK_UN)
def validate_input(input_data):"""验证输入数据的完整性"""required_fields = ['hook_event_name', 'session_id']for field in required_fields:if field not in input_data:raise ValueError(f"Missing required field: {field}")# 验证字段类型if not isinstance(input_data.get('tool_input'), dict):raise ValueError("tool_input must be a dictionary")return True
# ❌ 危险:直接执行os.system(f"echo {user_input}")# ✅ 安全:使用参数化import subprocesssubprocess.run(['echo', user_input], check=True)
import osdef safe_file_access(file_path, project_dir):"""确保文件路径在项目目录内"""abs_path = os.path.abspath(file_path)abs_project = os.path.abspath(project_dir)if not abs_path.startswith(abs_project):raise ValueError("Path traversal detected")return abs_path
# Hook 脚本应该以最小权限运行# 避免使用 sudo 或 root 权限# ✅ 检查权限if os.geteuid() == 0:print("Warning: Running as root is not recommended", file=sys.stderr)
def main():input_data = json.loads(sys.stdin.read())# 仅在特定条件下处理if not should_process(input_data):print(json.dumps({"continue": True}))return 0# 执行处理逻辑...
def main():input_data = json.loads(sys.stdin.read())# 应用多个规则for rule in RULES:if rule.matches(input_data):return rule.apply(input_data)# 默认行为print(json.dumps({"continue": True}))return 0
import requestsdef check_with_external_service(command):response = requests.post('https://api.example.com/validate',json={'command': command},timeout=5)return response.json()
settings.json 在 .codebuddy 目录下)hooks 字段配置正确,JSON 格式有效matcher 正则表达式能匹配目标chmod +x script.py)#!/usr/bin/env python3)timeout 配置值echo 手动传入测试数据sys.stderr 输出调试信息modifiedInput 字段permissionDecision 为 allowcontinue 为 trueconversationId 跟踪会话interface HookInput {// 通用字段session_id?: string; // 会话 IDtranscript_path?: string; // 对话记录路径cwd?: string; // 当前工作目录hook_event_name: string; // Hook 事件名称// SessionStart 专用(目前仅支持 'startup')source?: 'startup';// UserPromptSubmit 专用prompt?: string; // 用户输入内容// PreToolUse/PostToolUse 专用tool_name?: string; // 工具名称tool_input?: Record<string, any>; // 工具输入参数tool_response?: any; // 工具响应(仅 PostToolUse)// Stop 专用stop_hook_active?: boolean; // 是否已激活 Stop Hook// PreCompact 专用trigger?: 'manual' | 'auto'; // 触发方式custom_instructions?: string; // 自定义压缩指令}
interface HookOutput {// 基本控制continue?: boolean; // 是否继续执行(默认 true)stopReason?: string; // 停止原因suppressOutput?: boolean; // 是否隐藏输出systemMessage?: string; // 系统消息// Hook 特定输出hookSpecificOutput?: {hookEventName: string; // Hook 事件名称// PreToolUse 专用permissionDecision?: 'allow' | 'deny' | 'ask';permissionDecisionReason?: string;modifiedInput?: Record<string, any>;// SessionStart/UserPromptSubmit/PostToolUse 专用additionalContext?: string; // 额外上下文};}
环境变量 | 说明 | 示例值 |
CODEBUDDY_PROJECT_DIR | 项目根目录 | /path/to/project |
CLAUDE_PROJECT_DIR | 项目根目录(Claude Code 兼容) | /path/to/project |
退出码 | 含义 | stdout | stderr | 行为 |
0 | 成功 | 作为结果处理 | 忽略 | 继续执行,可能注入上下文 |
1 | 警告 | 忽略 | 作为警告显示 | 继续执行 |
2 | 阻止/反馈 | 忽略 | 传递给 Agent | PreToolUse: 阻止执行 <br>Stop: 提供反馈 |
其他 | 错误 | 忽略 | 作为警告显示 | 继续执行 |
read_file - 读取文件write_to_file - 写入文件replace_in_file - 替换文件内容delete_files - 删除文件list_files - 列出文件search_file - 搜索文件search_content - 搜索内容read_lints - 读取 Lint 错误execute_command - 执行命令preview_url - 预览 URLtask - 创建子任务web_search - 网络搜索web_fetch - 获取网页内容ask_followup_question - 询问用户文档反馈