Release Notes
conversationIdsourcestartup - Initial startup (currently only this value is supported){"session_id": "abc123","transcript_path": "/path/to/transcript.txt","cwd": "/project/path","hook_event_name": "SessionStart","source": "startup"}
{"continue": true,"hookSpecificOutput": {"hookEventName": "SessionStart","additionalContext": "Project uses TypeScript + React, prefer functional components"}}
{"hooks": {"SessionStart": [{"matcher": "startup","hooks": [{"type": "command","command": "/path/to/session_start.py","timeout": 30}]}]}}
reasonother - Session end (currently only this value is supported, including scenarios like switching sessions, deleting sessions, clearing sessions, etc.){"session_id": "abc123","transcript_path": "/path/to/transcript.txt","cwd": "/project/path","hook_event_name": "SessionEnd","reason": "other"}
{"continue": true,"systemMessage": "Session cleaned up, temporary files deleted"}
tool_nameexecute_command, write_to_file, read_filewrite_to_file|replace_in_file* or empty string{"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": "Added --legacy-peer-deps parameter","modifiedInput": {"command": "npm install --legacy-peer-deps","requires_approval": false}}}
{"continue": false,"hookSpecificOutput": {"hookEventName": "PreToolUse","permissionDecision": "deny","permissionDecisionReason": "Dangerous command detected: rm -rf /"}}
{"continue": true,"hookSpecificOutput": {"hookEventName": "PreToolUse","permissionDecision": "ask","permissionDecisionReason": "Detected git push --force, do you want to continue?"}}
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": "Tests passed, you can continue development"}}
{"session_id": "abc123","transcript_path": "/path/to/transcript.txt","cwd": "/project/path","hook_event_name": "UserPromptSubmit","prompt": "Help me implement a login feature"}
{"continue": true,"hookSpecificOutput": {"hookEventName": "UserPromptSubmit","additionalContext": "Tip: The project has integrated JWT authentication library, recommend using it"}}
{"continue": false,"stopReason": "Input contains sensitive information, blocked"}
{"session_id": "abc123","transcript_path": "/path/to/transcript.txt","cwd": "/project/path","hook_event_name": "Stop","stop_hook_active": false}
{"continue": false,"stopReason": "Please verify if the code passed unit tests"}
triggermanual - User manually triggers /summarizeauto - Automatic compaction{"session_id": "abc123","transcript_path": "/path/to/transcript.txt","cwd": "/project/path","hook_event_name": "PreCompact","trigger": "auto","custom_instructions": "Preserve all API design discussions"}
{"continue": true,"hookSpecificOutput": {"hookEventName": "PreCompact","additionalContext": "Important: Preserve database table structure design"}}
{"session_id": "Session ID","transcript_path": "Path to conversation transcript file","cwd": "Current working directory","hook_event_name": "Event name"}
SessionStart: sourceSessionEnd: reasonPreToolUse/PostToolUse: tool_name, tool_input, tool_responseUserPromptSubmit: promptPreCompact: trigger, custom_instructionsStop: stop_hook_active{"continue": true,"suppressOutput": false,"systemMessage": "Optional system message","stopReason": "Reason for blocking (when continue=false)","hookSpecificOutput": {"hookEventName": "Event name","permissionDecision": "allow|deny|ask","permissionDecisionReason": "Decision reason","modifiedInput": {},"additionalContext": "Additional context"}}
continue: Whether to allow the operation to continue (false means block)suppressOutput: Whether to hide stdout outputsystemMessage: System message displayed to the userstopReason: Reason for blockinghookSpecificOutput: Event-specific output dataExit Code | Meaning | Behavior |
0 | Success | Allow operation to continue, stdout may be processed |
1 | Non-blocking error | Display stderr as warning, allow to continue |
2 | Blocking error | Block operation, stderr passed to Agent/model |
Other | Non-blocking error | Same as exit code 1 |
CLAUDE_PROJECT_DIR: Project root directory (Claude Code compatible)CODEBUDDY_PROJECT_DIR: Project root directory (CodeBuddy specific)<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}]}]}}
"" or "*": Match all"execute_command""write_to_file|replace_in_file""read.*|search.*"PreToolUse/PostToolUse: Matches tool_nameSessionStart: Matches sourceSessionEnd: Matches reasonPreCompact: Matches triggerUserPromptSubmit/Stop: Matcher not used"$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/script.py""python3 /path/to/script.py"60 secondsrm -rf commandsvalidate_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 command detected: {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}]}]}}
--legacy-peer-deps parameter to npm installmodify_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', '')# Check if it's npm installif re.match(r'^npm\\s+(i|install)\\b', command.strip()):# If --legacy-peer-deps is not present, add itif '--legacy-peer-deps' not in command:modified_command = command.strip() + ' --legacy-peer-deps'output = {"continue": True,"hookSpecificOutput": {"hookEventName": "PreToolUse","permissionDecision": "allow","permissionDecisionReason": "Automatically added --legacy-peer-deps parameter","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', '')# Only process file write toolsif 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# Create backup directoryproject_dir = os.environ.get('CODEBUDDY_PROJECT_DIR', '')backup_dir = os.path.join(project_dir, '.codebuddy', 'backups')os.makedirs(backup_dir, exist_ok=True)# Generate backup filenametimestamp = 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)# Create backupshutil.copy2(file_path, backup_path)output = {"continue": True,"systemMessage": f"Backed up to: {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', '')# Read project configurationconfig_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"""Project Name: {config.get('name', 'Unknown')}Tech Stack: {', '.join(config.get('tech_stack', []))}Coding Standard: {config.get('coding_standard', 'Standard')}"""output = {"continue": True,"hookSpecificOutput": {"hookEventName": "SessionStart","additionalContext": f"""Session started!Project Directory: {project_dir}Startup Source: {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())# Only process automatic compactionif 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# Create save directorysave_dir = os.path.join(project_dir, '.codebuddy', 'context_history')os.makedirs(save_dir, exist_ok=True)# Save conversation historytimestamp = 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"Context saved to: {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 configured successfully!'}}))\\"","timeout": 5}]}]}}EOF
# Create Hook script directorymkdir -p ~/.codebuddy/hooks# Create test scriptcat > ~/.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"Welcome to Agent-Craft! Current project: {input_data.get('cwd', 'unknown')}"}}print(json.dumps(output, ensure_ascii=False))return 0if __name__ == "__main__":sys.exit(main())EOF# Add execution permissionchmod +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):"""Write to debug log without affecting stdout"""with open('/tmp/hook_debug.log', 'a') as f:f.write(f"{message}\\n")# Use in Hook scriptdebug_log(f"Received input: {json.dumps(input_data)}")
# Test if script output JSON is validecho '{"hook_event_name":"SessionStart"}' | python3 your_hook.py | jq .
# Watch Hook logs in real-timetail -f ~/.codebuddy/logs/agent-craft.log | grep -i hook
import os# Get project directory in Hookproject_dir = os.environ.get('CODEBUDDY_PROJECT_DIR', '')claude_dir = os.environ.get('CLAUDE_PROJECT_DIR', '') # Claude Code compatible
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):"""Automatically add common parameters"""enhancements = {'npm install': ' --legacy-peer-deps','git push': ' --dry-run', # Safe mode}for prefix, suffix in enhancements.items():if command.startswith(prefix) and suffix not in command:return command + suffixreturn command
def should_block(input_data):"""Determine whether to block based on multiple conditions"""tool_name = input_data.get('tool_name')tool_input = input_data.get('tool_input', {})# Rule 1: Block deletion of important filesif 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, "Cannot delete important files"# Rule 2: Block dangerous commandsif tool_name == 'execute_command':command = tool_input.get('command', '')if 'rm -rf /' in command or 'dd if=' in command:return True, "Dangerous command detected"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 for debug output, don't pollute stdoutecho '{"hook_event_name":"PreToolUse","tool_name":"execute_command","tool_input":{"command":"npm install"}}' | \\python3 /path/to/your_hook.py
# Output debug information in Hook scriptimport syssys.stderr.write(f"[DEBUG] Processing command: {command}\\n")sys.stderr.flush()
<workspace>/.codebuddy/, version control with project~/.codebuddy/, reuse across projects# Bad practice: Load large file every timedef main():with open('huge_config.json', 'r') as f:config = json.load(f) # Read every time# ... processing logic# Good practice: Cache configurationCONFIG_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):"""Thread-safe log writing"""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):"""Validate input data integrity"""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}")# Validate field typesif not isinstance(input_data.get('tool_input'), dict):raise ValueError("tool_input must be a dictionary")return True
# Dangerous: Direct executionos.system(f"echo {user_input}")# Safe: Use parameterized approachimport subprocesssubprocess.run(['echo', user_input], check=True)
import osdef safe_file_access(file_path, project_dir):"""Ensure file path is within project directory"""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 scripts should run with minimum privileges# Avoid using sudo or root permissions# Check privilegesif os.geteuid() == 0:print("Warning: Running as root is not recommended", file=sys.stderr)
def main():input_data = json.loads(sys.stdin.read())# Only process under specific conditionsif not should_process(input_data):print(json.dumps({"continue": True}))return 0# Execute processing logic...
def main():input_data = json.loads(sys.stdin.read())# Apply multiple rulesfor rule in RULES:if rule.matches(input_data):return rule.apply(input_data)# Default behaviorprint(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 in .codebuddy directory)hooks field is configured correctly, JSON format is validmatcher regex can match the targetchmod +x script.py)#!/usr/bin/env python3)timeout configuration valueecho to manually pass test datasys.stderr for debug output in the scriptmodifiedInput field is returnedpermissionDecision is allowcontinue is trueconversationIdinterface HookInput {// Common fieldssession_id?: string; // Session IDtranscript_path?: string; // Conversation transcript pathcwd?: string; // Current working directoryhook_event_name: string; // Hook event name// SessionStart specific (currently only 'startup' is supported)source?: 'startup';// UserPromptSubmit specificprompt?: string; // User input content// PreToolUse/PostToolUse specifictool_name?: string; // Tool nametool_input?: Record<string, any>; // Tool input parameterstool_response?: any; // Tool response (PostToolUse only)// Stop specificstop_hook_active?: boolean; // Whether Stop Hook is activated// PreCompact specifictrigger?: 'manual' | 'auto'; // Trigger methodcustom_instructions?: string; // Custom compaction instructions}
interface HookOutput {// Basic controlcontinue?: boolean; // Whether to continue execution (default true)stopReason?: string; // Stop reasonsuppressOutput?: boolean; // Whether to hide outputsystemMessage?: string; // System message// Hook-specific outputhookSpecificOutput?: {hookEventName: string; // Hook event name// PreToolUse specificpermissionDecision?: 'allow' | 'deny' | 'ask';permissionDecisionReason?: string;modifiedInput?: Record<string, any>;// SessionStart/UserPromptSubmit/PostToolUse specificadditionalContext?: string; // Additional context};}
Environment Variable | Description | Example Value |
CODEBUDDY_PROJECT_DIR | Project root directory | /path/to/project |
CLAUDE_PROJECT_DIR | Project root directory (Claude Code compatible) | /path/to/project |
Exit Code | Meaning | stdout | stderr | Behavior |
0 | Success | Processed as result | Ignored | Continue execution, may inject context |
1 | Warning | Ignored | Displayed as warning | Continue execution |
2 | Block/Feedback | Ignored | Passed to Agent | PreToolUse: Block execution<br>Stop: Provide feedback |
Other | Error | Ignored | Displayed as warning | Continue execution |
read_file - Read filewrite_to_file - Write to filereplace_in_file - Replace file contentdelete_files - Delete fileslist_files - List filessearch_file - Search filessearch_content - Search contentread_lints - Read Lint errorsexecute_command - Execute commandpreview_url - Preview URLtask - Create subtaskweb_search - Web searchweb_fetch - Fetch web contentask_followup_question - Ask userフィードバック