Compare commits

...
3 changed files with 576 additions and 265 deletions
@@ -1,66 +1,69 @@
#!/usr/bin/env node
/**
* TEMPLATE HOOK SCRIPT
* Hook Template Script
*
* This is a template for creating new hook fixtures.
* Copy this file to create a new fixture script.
* This is a template for creating new hook test fixtures.
* Copy this file and customize the logic for your specific test case.
*
* Customize the logic below to implement your specific hook behavior.
* Available input data (depends on hook type):
* - Common: clineVersion, hookName, timestamp, taskId, workspaceRoots, userId
* - PreToolUse: preToolUse.toolName, preToolUse.parameters
* - PostToolUse: postToolUse.toolName, postToolUse.parameters, postToolUse.result, postToolUse.success, postToolUse.executionTimeMs
* - UserPromptSubmit: userPromptSubmit.prompt, userPromptSubmit.attachments
* - TaskStart/Resume/Cancel/Complete: taskX.taskMetadata
* - PreCompact: preCompact.contextSize, preCompact.messagesToCompact, preCompact.compactionStrategy
*/
try {
// Parse the input from stdin (what gets passed to the hook)
// Parse input from stdin
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
// Extract relevant input data
// For PreToolUse hooks:
const { toolName, parameters } = input.preToolUse || {};
// For PostToolUse hooks:
// const { toolName, parameters, result, success, executionTimeMs } = input.postToolUse || {};
// Common metadata (available in all hook types)
const { hookName: hookType, timestamp, taskId, workspaceRoots, userId } = input;
const { hookName, timestamp, taskId, workspaceRoots, userId, clineVersion } = input;
// Initialize output variables
let shouldContinue = true;
// Hook-specific data (only one of these will be present)
const { preToolUse, postToolUse, userPromptSubmit, taskStart, taskResume, taskCancel, taskComplete, preCompact } = input;
// Initialize output
let cancel = false;
let contextModification = "";
let errorMessage = "";
// === CUSTOMIZE THIS LOGIC ===
// Implement your hook logic here
// Example: Simple success hook
// === CUSTOMIZE THIS SECTION ===
// Example: Success case
contextModification = "TEMPLATE: Hook executed successfully";
// Example: Context injection based on tool name
if (toolName === "write_to_file") {
contextModification = "FILE_OPERATIONS: File modification operation";
} else if (toolName === "run_command") {
contextModification = "SYSTEM_OPERATIONS: Command execution operation";
// Example: PreToolUse - block specific tools
if (preToolUse) {
const { toolName, parameters } = preToolUse;
// Uncomment to block a specific tool:
// if (toolName === "execute_command") {
// cancel = true;
// errorMessage = "Tool blocked by hook";
// }
}
// Example: Validation/blocking
// if (!parameters?.path) {
// shouldContinue = false;
// errorMessage = "ERROR: Tool requires a 'path' parameter";
// }
// Example: PostToolUse - track results
if (postToolUse) {
const { toolName, success, executionTimeMs } = postToolUse;
contextModification = `POST_TOOL: ${toolName} completed (success: ${success})`;
}
// === END CUSTOM LOGIC ===
// === END CUSTOMIZATION ===
// Return the standardized output format
// Output result (must be valid JSON)
console.log(JSON.stringify({
shouldContinue,
cancel,
contextModification,
errorMessage
}));
} catch (error) {
// Error handling - hooks should handle their own errors gracefully
const errorMessage = error instanceof Error ? error.message : String(error);
// Error handling - output valid JSON even on error
console.log(JSON.stringify({
cancel: true,
contextModification: "",
errorMessage: `HOOK_ERROR: ${errorMessage}`
errorMessage: `HOOK_ERROR: ${error instanceof Error ? error.message : String(error)}`
}));
}
@@ -13,9 +13,9 @@ This directory contains a template for creating new hook fixtures. When adding a
Decide what your hook fixture should test:
- `success` - Returns success immediately
- `blocking` - Blocks tool execution
- `context-injection` - Adds context information
- `error` - Exits with error code
- `blocking` - Blocks tool/task execution (sets `cancel: true`)
- `context-injection` - Adds context information via `contextModification`
- `error` - Exits with error code or returns error message
### Step 2: Create the Directory Structure
@@ -39,16 +39,21 @@ Edit the new fixture file to implement your specific logic:
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
// Extract relevant data
const { toolName, parameters } = input.preToolUse;
// Extract relevant data based on hook type
const { preToolUse } = input;
const { toolName, parameters } = preToolUse || {};
let shouldContinue = true;
// Common metadata (available in all hooks)
const { hookName, timestamp, taskId, workspaceRoots, userId } = input;
// Initialize output
let cancel = false;
let contextModification = "";
let errorMessage = "";
// Your custom logic here
if (!parameters || !parameters.path) {
shouldContinue = false;
if (!parameters?.path) {
cancel = true;
errorMessage = "ERROR: Tool requires a 'path' parameter";
} else {
contextModification = "VALIDATION: Basic input validation passed";
@@ -56,7 +61,7 @@ if (!parameters || !parameters.path) {
// Return standardized output
console.log(JSON.stringify({
shouldContinue,
cancel,
contextModification,
errorMessage
}));
@@ -70,6 +75,82 @@ Add your new fixture to `fixtures/README.md` with:
- What it's used for testing
- Any special behavior notes
## Hook Output Schema
All hooks must return JSON with these fields:
```json
{
"cancel": false, // boolean - true to block the operation
"contextModification": "", // string - text to add to conversation context
"errorMessage": "" // string - error message shown when cancel is true
}
```
## Hook Input Data by Type
### Common Fields (all hooks)
- `clineVersion`: string - Cline extension version
- `hookName`: string - Type of hook (e.g., "PreToolUse")
- `timestamp`: string - Execution timestamp in milliseconds
- `taskId`: string - Unique task identifier
- `workspaceRoots`: string[] - Array of workspace folder paths
- `userId`: string - User identifier
### PreToolUse
```json
{
"preToolUse": {
"toolName": "execute_command",
"parameters": { "command": "npm install" }
}
}
```
### PostToolUse
```json
{
"postToolUse": {
"toolName": "write_to_file",
"parameters": { "path": "src/index.ts" },
"result": "File written successfully",
"success": true,
"executionTimeMs": 150
}
}
```
### UserPromptSubmit
```json
{
"userPromptSubmit": {
"prompt": "Add a new feature",
"attachments": []
}
}
```
### TaskStart / TaskResume / TaskCancel / TaskComplete
```json
{
"taskStart": {
"taskMetadata": { "task": "Create a new feature..." }
}
}
```
(Replace `taskStart` with `taskResume`, `taskCancel`, or `taskComplete` as needed)
### PreCompact
```json
{
"preCompact": {
"contextSize": 180000,
"messagesToCompact": 45,
"compactionStrategy": "half"
}
}
```
## Best Practices
### Keep Fixtures Focused
@@ -91,6 +172,6 @@ Add your new fixture to `fixtures/README.md` with:
See the existing fixtures for real-world examples:
- `../hooks/pretooluse/success/` - Simple success case
- `../hooks/pretooluse/blocking/` - How to block execution
- `../hooks/pretooluse/blocking/` - How to block execution with `cancel: true`
- `../hooks/pretooluse/context-injection/` - How to inject context
- `../hooks/pretooluse/error/` - How to return errors
+447 -220
View File
@@ -1,7 +1,7 @@
/**
* Hook script templates for all supported hook types.
* Templates are provided as executable Bash shell scripts with comprehensive examples.
* Scripts use jq for JSON parsing when available, with fallback to basic parsing.
* Templates are provided as executable Bash shell scripts with cross-platform compatibility.
* Scripts work out of the box without external dependencies (jq is optional).
*/
export function getHookTemplate(hookName: string): string {
@@ -21,334 +21,561 @@ export function getHookTemplate(hookName: string): string {
function getTaskStartTemplate(): string {
return `#!/bin/bash
#
# ============================================================================
# TaskStart Hook
#
# Executes when a new task begins.
#
# Input: { taskId, taskStart: { task: string }, clineVersion, timestamp, ... }
# Output: { cancel: boolean, contextModification?: string, errorMessage?: string }
#
# Use cases:
# - Log task start time
# - Add context about environment or project state
# - Check prerequisites before starting
# - Notify external systems (Slack, issue trackers, etc.)
# Runs when a new task begins.
# ============================================================================
#
# EXAMPLE INPUT (JSON via stdin):
# {
# "clineVersion": "3.17.0",
# "hookName": "TaskStart",
# "taskId": "abc123",
# "timestamp": "1749484935515",
# "workspaceRoots": ["/path/to/project"],
# "userId": "user-123",
# "taskStart": {
# "taskMetadata": {
# "task": "Create a new feature..."
# }
# }
# }
#
# OUTPUT: JSON with { cancel, contextModification, errorMessage }
# - cancel: true to abort the task before it starts
# - contextModification: text to add to the conversation context
# - errorMessage: shown to user if cancel is true
# ============================================================================
# Read JSON input from stdin
# Read input from stdin
INPUT=$(cat)
# Parse input using jq (or fallback to basic parsing)
if command -v jq &> /dev/null; then
TASK=$(echo "$INPUT" | jq -r '.taskStart.task')
TASK_ID=$(echo "$INPUT" | jq -r '.taskId')
TIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')
else
# Fallback if jq is not available
TASK="<task>"
TASK_ID="<taskId>"
TIMESTAMP=$(date +%s%3N)
# --- Parse JSON (works with or without jq) ---
get_json_value() {
local key="$1"
if command -v jq >/dev/null 2>&1; then
echo "$INPUT" | jq -r "$key // empty" 2>/dev/null
else
# Simple grep fallback for basic string values
echo "$INPUT" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*: *"\\([^"]*\\)".*/\\1/'
fi
}
TASK_ID=$(get_json_value "taskId")
TIMESTAMP=$(get_json_value "timestamp")
# --- Example: Log task start to a file ---
# Uncomment to enable logging:
# LOG_FILE="\${HOME}/.cline/task-log.txt"
# mkdir -p "$(dirname "$LOG_FILE")"
# echo "[\$(date '+%Y-%m-%d %H:%M:%S')] Task started: $TASK_ID" >> "$LOG_FILE"
# --- Example: Add project context ---
# Check for project-specific information and inject it into the conversation.
# This is useful for adding rules, conventions, or current state.
CONTEXT=""
# Check for package.json to identify Node.js projects
if [ -f "package.json" ]; then
PROJECT_NAME=$(get_json_value "name" < package.json 2>/dev/null || echo "")
if [ -n "$PROJECT_NAME" ]; then
CONTEXT="Project: $PROJECT_NAME (Node.js)"
fi
fi
# Example: Log task start
echo "[TaskStart] Task started: $TASK" >&2
echo "[TaskStart] Task ID: $TASK_ID" >&2
# Check for .git to add branch info
if [ -d ".git" ]; then
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
if [ -n "$BRANCH" ]; then
CONTEXT="\${CONTEXT:+$CONTEXT | }Git branch: $BRANCH"
fi
fi
# Example: Add context to the task
TIMESTAMP_ISO=$(date -u -d @"$((TIMESTAMP/1000))" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
CONTEXT_MOD="Note: Task started at $TIMESTAMP_ISO"
# Return result as JSON
echo "{\"cancel\":false,\"contextModification\":\"$CONTEXT_MOD\",\"errorMessage\":\"\"}"
# --- Output result ---
# Using printf for cross-platform JSON output
printf '{"cancel":false,"contextModification":"%s","errorMessage":""}' "$CONTEXT"
`
}
function getTaskResumeTemplate(): string {
return `#!/bin/bash
#
# ============================================================================
# TaskResume Hook
#
# Executes when a task is resumed after being interrupted.
#
# Input: { taskId, taskResume: { task: string }, clineVersion, timestamp, ... }
# Output: { cancel: boolean, contextModification?: string, errorMessage?: string }
#
# Use cases:
# - Check for changes since task was paused
# - Refresh context with latest project state
# - Notify that work is resuming
# Runs when a task is resumed after being paused.
# ============================================================================
#
# EXAMPLE INPUT (JSON via stdin):
# {
# "clineVersion": "3.17.0",
# "hookName": "TaskResume",
# "taskId": "abc123",
# "timestamp": "1749484935515",
# "workspaceRoots": ["/path/to/project"],
# "userId": "user-123",
# "taskResume": {
# "taskMetadata": { "task": "..." },
# "previousState": { "lastActiveTime": "..." }
# }
# }
#
# OUTPUT: JSON with { cancel, contextModification, errorMessage }
# ============================================================================
# Read JSON input from stdin
INPUT=$(cat)
# Parse input using jq (or fallback to basic parsing)
if command -v jq &> /dev/null; then
TASK=$(echo "$INPUT" | jq -r '.taskResume.task')
else
TASK="<task>"
# --- Parse JSON ---
get_json_value() {
local key="$1"
if command -v jq >/dev/null 2>&1; then
echo "$INPUT" | jq -r "$key // empty" 2>/dev/null
else
echo "$INPUT" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*: *"\\([^"]*\\)".*/\\1/'
fi
}
TASK_ID=$(get_json_value "taskId")
# --- Example: Check what changed since the task was paused ---
CONTEXT=""
# Check for uncommitted git changes
if [ -d ".git" ]; then
CHANGED_FILES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
if [ "$CHANGED_FILES" -gt 0 ]; then
CONTEXT="Note: $CHANGED_FILES file(s) have uncommitted changes since this task was paused."
fi
fi
echo "[TaskResume] Resuming task: $TASK" >&2
# --- Example: Check if dependencies changed ---
# Uncomment to detect dependency changes:
# if [ -f "package-lock.json" ]; then
# if ! git diff --quiet package-lock.json 2>/dev/null; then
# CONTEXT="\${CONTEXT:+$CONTEXT }Dependencies may have changed - consider running npm install."
# fi
# fi
# Return result
echo "{\"cancel\":false,\"contextModification\":\"\",\"errorMessage\":\"\"}"
printf '{"cancel":false,"contextModification":"%s","errorMessage":""}' "$CONTEXT"
`
}
function getTaskCancelTemplate(): string {
return `#!/bin/bash
#
# ============================================================================
# TaskCancel Hook
#
# Executes when a task is cancelled by the user.
#
# Input: { taskId, taskCancel: { task: string }, clineVersion, timestamp, ... }
# Output: { cancel: boolean, contextModification?: string, errorMessage?: string }
#
# Use cases:
# - Clean up temporary files or resources
# - Notify external systems about cancellation
# - Log cancellation for analytics
# Runs when the user cancels a task.
# ============================================================================
#
# EXAMPLE INPUT (JSON via stdin):
# {
# "clineVersion": "3.17.0",
# "hookName": "TaskCancel",
# "taskId": "abc123",
# "timestamp": "1749484935515",
# "workspaceRoots": ["/path/to/project"],
# "userId": "user-123",
# "taskCancel": {
# "taskMetadata": { "task": "..." }
# }
# }
#
# OUTPUT: JSON with { cancel, contextModification, errorMessage }
# Note: "cancel" in output doesn't prevent cancellation - it's already happening.
# ============================================================================
# Read JSON input from stdin
INPUT=$(cat)
# Parse input using jq (or fallback to basic parsing)
if command -v jq &> /dev/null; then
TASK=$(echo "$INPUT" | jq -r '.taskCancel.task')
else
TASK="<task>"
fi
# --- Parse JSON ---
get_json_value() {
local key="$1"
if command -v jq >/dev/null 2>&1; then
echo "$INPUT" | jq -r "$key // empty" 2>/dev/null
else
echo "$INPUT" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*: *"\\([^"]*\\)".*/\\1/'
fi
}
echo "[TaskCancel] Task cancelled: $TASK" >&2
TASK_ID=$(get_json_value "taskId")
# Return result
echo "{\"cancel\":false,\"contextModification\":\"\",\"errorMessage\":\"\"}"
# --- Example: Log cancellation ---
# LOG_FILE="\${HOME}/.cline/task-log.txt"
# mkdir -p "$(dirname "$LOG_FILE")"
# echo "[\$(date '+%Y-%m-%d %H:%M:%S')] Task cancelled: $TASK_ID" >> "$LOG_FILE"
# --- Example: Clean up temporary files ---
# Uncomment to clean up task-specific temp files:
# TEMP_DIR="/tmp/cline-$TASK_ID"
# if [ -d "$TEMP_DIR" ]; then
# rm -rf "$TEMP_DIR"
# fi
printf '{"cancel":false,"contextModification":"","errorMessage":""}'
`
}
function getTaskCompleteTemplate(): string {
return `#!/bin/bash
#
# ============================================================================
# TaskComplete Hook
#
# Executes when a task completes successfully.
#
# Input: { taskId, taskComplete: { task: string }, clineVersion, timestamp, ... }
# Output: { cancel: boolean, contextModification?: string, errorMessage?: string }
#
# Use cases:
# - Run tests or validation
# - Generate reports or summaries
# - Notify stakeholders
# - Trigger CI/CD pipelines
# Runs when a task completes successfully.
# ============================================================================
#
# EXAMPLE INPUT (JSON via stdin):
# {
# "clineVersion": "3.17.0",
# "hookName": "TaskComplete",
# "taskId": "abc123",
# "timestamp": "1749484935515",
# "workspaceRoots": ["/path/to/project"],
# "userId": "user-123",
# "taskComplete": {
# "taskMetadata": { "task": "..." }
# }
# }
#
# OUTPUT: JSON with { cancel, contextModification, errorMessage }
# ============================================================================
# Read JSON input from stdin
INPUT=$(cat)
# Parse input using jq (or fallback to basic parsing)
if command -v jq &> /dev/null; then
TASK=$(echo "$INPUT" | jq -r '.taskComplete.task')
else
TASK="<task>"
# --- Parse JSON ---
get_json_value() {
local key="$1"
if command -v jq >/dev/null 2>&1; then
echo "$INPUT" | jq -r "$key // empty" 2>/dev/null
else
echo "$INPUT" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*: *"\\([^"]*\\)".*/\\1/'
fi
}
TASK_ID=$(get_json_value "taskId")
# --- Example: Log completion ---
# LOG_FILE="\${HOME}/.cline/task-log.txt"
# mkdir -p "$(dirname "$LOG_FILE")"
# echo "[\$(date '+%Y-%m-%d %H:%M:%S')] Task completed: $TASK_ID" >> "$LOG_FILE"
# --- Example: Run tests after task completion ---
# Uncomment to run tests when task completes:
# if [ -f "package.json" ]; then
# npm test 2>&1 | head -20
# fi
# --- Example: Show git diff summary ---
CONTEXT=""
if [ -d ".git" ]; then
CHANGES=$(git diff --stat HEAD~1 2>/dev/null | tail -1)
if [ -n "$CHANGES" ]; then
CONTEXT="Changes in this session: $CHANGES"
fi
fi
echo "[TaskComplete] Task completed: $TASK" >&2
# Return result
echo "{\"cancel\":false,\"contextModification\":\"\",\"errorMessage\":\"\"}"
printf '{"cancel":false,"contextModification":"%s","errorMessage":""}' "$CONTEXT"
`
}
function getPreToolUseTemplate(): string {
return `#!/bin/bash
#
# ============================================================================
# PreToolUse Hook
#
# Executes before any tool is used (read_file, write_to_file, execute_command, etc.)
#
# Input: { taskId, preToolUse: { tool: string, parameters: object }, ... }
# Output: { cancel: boolean, contextModification?: string, errorMessage?: string }
#
# Use cases:
# - Block dangerous operations
# - Add safety checks before file modifications
# - Log tool usage
# - Validate parameters before execution
# Runs BEFORE a tool is executed. Can block tool execution.
# ============================================================================
#
# EXAMPLE INPUT (JSON via stdin):
# {
# "clineVersion": "3.17.0",
# "hookName": "PreToolUse",
# "taskId": "abc123",
# "timestamp": "1749484935515",
# "workspaceRoots": ["/path/to/project"],
# "userId": "user-123",
# "preToolUse": {
# "toolName": "execute_command",
# "parameters": {
# "command": "npm install"
# }
# }
# }
#
# OUTPUT: JSON with { cancel, contextModification, errorMessage }
# - Set cancel=true to BLOCK the tool from running
# - errorMessage explains why the tool was blocked
# ============================================================================
# Read JSON input from stdin
INPUT=$(cat)
# Parse input using jq (or fallback to basic parsing)
if command -v jq &> /dev/null; then
TOOL=$(echo "$INPUT" | jq -r '.preToolUse.tool')
# --- Parse JSON ---
if command -v jq >/dev/null 2>&1; then
TOOL_NAME=$(echo "$INPUT" | jq -r '.preToolUse.toolName // empty')
COMMAND=$(echo "$INPUT" | jq -r '.preToolUse.parameters.command // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.preToolUse.parameters.path // empty')
else
TOOL="<tool>"
COMMAND=""
# Fallback parsing for common fields
TOOL_NAME=$(echo "$INPUT" | grep -o '"toolName"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"toolName"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
COMMAND=$(echo "$INPUT" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
FILE_PATH=$(echo "$INPUT" | grep -o '"path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"path"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
fi
# Example: Block dangerous operations
if [[ "$TOOL" == "execute_command" ]] && [[ "$COMMAND" == *"rm -rf /"* ]]; then
echo "{\"cancel\":true,\"errorMessage\":\"Dangerous command blocked by PreToolUse hook\"}"
exit 0
# --- Safety checks ---
CANCEL="false"
ERROR_MSG=""
CONTEXT=""
# Block dangerous commands
if [ "$TOOL_NAME" = "execute_command" ]; then
# Block recursive force delete at root
if echo "$COMMAND" | grep -qE 'rm[[:space:]]+-rf[[:space:]]+/[^/]|rm[[:space:]]+-rf[[:space:]]+/$'; then
CANCEL="true"
ERROR_MSG="Blocked: Dangerous recursive delete command"
fi
# Block commands that could expose secrets
if echo "$COMMAND" | grep -qiE 'curl.*(-d|--data).*password|wget.*password'; then
CANCEL="true"
ERROR_MSG="Blocked: Command may expose sensitive data"
fi
fi
# Example: Log tool usage
echo "[PreToolUse] Tool about to execute: $TOOL" >&2
# --- Example: Warn about file operations outside workspace ---
# Uncomment to add warnings for file operations:
# if [ "$TOOL_NAME" = "write_to_file" ] && [ -n "$FILE_PATH" ]; then
# case "$FILE_PATH" in
# /*) CONTEXT="Note: Writing to absolute path outside workspace" ;;
# esac
# fi
# Allow execution
echo "{\"cancel\":false,\"contextModification\":\"\",\"errorMessage\":\"\"}"
# --- Output result ---
if [ "$CANCEL" = "true" ]; then
printf '{"cancel":true,"contextModification":"","errorMessage":"%s"}' "$ERROR_MSG"
else
printf '{"cancel":false,"contextModification":"%s","errorMessage":""}' "$CONTEXT"
fi
`
}
function getPostToolUseTemplate(): string {
return `#!/bin/bash
#
# ============================================================================
# PostToolUse Hook
#
# Executes after any tool is used successfully or fails.
#
# Input: {
# taskId,
# postToolUse: {
# tool: string,
# parameters: object,
# result: string,
# success: boolean,
# durationMs: number
# },
# ...
# Runs AFTER a tool has executed (success or failure).
# ============================================================================
#
# EXAMPLE INPUT (JSON via stdin):
# {
# "clineVersion": "3.17.0",
# "hookName": "PostToolUse",
# "taskId": "abc123",
# "timestamp": "1749484935515",
# "workspaceRoots": ["/path/to/project"],
# "userId": "user-123",
# "postToolUse": {
# "toolName": "write_to_file",
# "parameters": { "path": "src/index.ts" },
# "result": "File written successfully",
# "success": true,
# "executionTimeMs": 150
# }
# }
# Output: { cancel: boolean, contextModification?: string, errorMessage?: string }
#
# Use cases:
# - Audit tool usage
# - Validate results
# - Trigger follow-up actions
# - Monitor performance
#
# OUTPUT: JSON with { cancel, contextModification, errorMessage }
# ============================================================================
# Read JSON input from stdin
INPUT=$(cat)
# Parse input using jq (or fallback to basic parsing)
if command -v jq &> /dev/null; then
TOOL=$(echo "$INPUT" | jq -r '.postToolUse.tool')
SUCCESS=$(echo "$INPUT" | jq -r '.postToolUse.success')
DURATION=$(echo "$INPUT" | jq -r '.postToolUse.durationMs')
# --- Parse JSON ---
if command -v jq >/dev/null 2>&1; then
TOOL_NAME=$(echo "$INPUT" | jq -r '.postToolUse.toolName // empty')
SUCCESS=$(echo "$INPUT" | jq -r '.postToolUse.success // empty')
EXEC_TIME=$(echo "$INPUT" | jq -r '.postToolUse.executionTimeMs // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.postToolUse.parameters.path // empty')
else
TOOL="<tool>"
SUCCESS="true"
DURATION="0"
TOOL_NAME=$(echo "$INPUT" | grep -o '"toolName"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"toolName"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
SUCCESS=$(echo "$INPUT" | grep -o '"success"[[:space:]]*:[[:space:]]*[a-z]*' | head -1 | sed 's/.*:[[:space:]]*//')
EXEC_TIME=$(echo "$INPUT" | grep -o '"executionTimeMs"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
FILE_PATH=$(echo "$INPUT" | grep -o '"path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"path"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
fi
# Log tool completion
STATUS="success"
[[ "$SUCCESS" == "false" ]] && STATUS="failed"
echo "[PostToolUse] Tool completed: $TOOL ($STATUS) in \${DURATION}ms" >&2
# --- Example: Log tool usage ---
# LOG_FILE="\${HOME}/.cline/tool-log.txt"
# mkdir -p "$(dirname "$LOG_FILE")"
# echo "[\$(date '+%Y-%m-%d %H:%M:%S')] $TOOL_NAME: success=$SUCCESS time=\${EXEC_TIME}ms" >> "$LOG_FILE"
# Return result
echo "{\"cancel\":false,\"contextModification\":\"\",\"errorMessage\":\"\"}"
# --- Example: Track slow operations ---
CONTEXT=""
if [ -n "$EXEC_TIME" ] && [ "$EXEC_TIME" -gt 5000 ] 2>/dev/null; then
CONTEXT="Note: $TOOL_NAME took \${EXEC_TIME}ms (>5s)"
fi
# --- Example: Notify about file changes ---
# Uncomment to add context after file modifications:
# if [ "$TOOL_NAME" = "write_to_file" ] && [ "$SUCCESS" = "true" ]; then
# CONTEXT="File modified: $FILE_PATH"
# fi
printf '{"cancel":false,"contextModification":"%s","errorMessage":""}' "$CONTEXT"
`
}
function getUserPromptSubmitTemplate(): string {
return `#!/bin/bash
#
# ============================================================================
# UserPromptSubmit Hook
#
# Executes when the user submits a prompt to Cline.
#
# Input: { taskId, userPromptSubmit: { prompt: string }, clineVersion, timestamp, ... }
# Output: { cancel: boolean, contextModification?: string, errorMessage?: string }
#
# Use cases:
# - Log user prompts for analytics
# - Add context based on prompt content
# - Validate or sanitize prompts
# - Trigger external integrations
# Runs when the user submits a prompt to Cline.
# ============================================================================
#
# EXAMPLE INPUT (JSON via stdin):
# {
# "clineVersion": "3.17.0",
# "hookName": "UserPromptSubmit",
# "taskId": "abc123",
# "timestamp": "1749484935515",
# "workspaceRoots": ["/path/to/project"],
# "userId": "user-123",
# "userPromptSubmit": {
# "prompt": "Add a new login feature",
# "attachments": []
# }
# }
#
# OUTPUT: JSON with { cancel, contextModification, errorMessage }
# - Set cancel=true to block the prompt from being sent
# ============================================================================
# Read JSON input from stdin
INPUT=$(cat)
# Parse input using jq (or fallback to basic parsing)
if command -v jq &> /dev/null; then
PROMPT=$(echo "$INPUT" | jq -r '.userPromptSubmit.prompt')
PROMPT_LENGTH=\${#PROMPT}
# --- Parse JSON ---
if command -v jq >/dev/null 2>&1; then
PROMPT=$(echo "$INPUT" | jq -r '.userPromptSubmit.prompt // empty')
TASK_ID=$(echo "$INPUT" | jq -r '.taskId // empty')
else
PROMPT_LENGTH=0
PROMPT=$(echo "$INPUT" | grep -o '"prompt"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"prompt"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
TASK_ID=$(echo "$INPUT" | grep -o '"taskId"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"taskId"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
fi
echo "[UserPromptSubmit] User submitted prompt (length: $PROMPT_LENGTH)" >&2
# --- Example: Log prompts for analytics ---
# LOG_FILE="\${HOME}/.cline/prompt-log.txt"
# mkdir -p "$(dirname "$LOG_FILE")"
# echo "[\$(date '+%Y-%m-%d %H:%M:%S')] $TASK_ID: $PROMPT" >> "$LOG_FILE"
# Return result
echo "{\"cancel\":false,\"contextModification\":\"\",\"errorMessage\":\"\"}"
# --- Example: Add context based on prompt content ---
CONTEXT=""
# Detect if user is asking about tests
if echo "$PROMPT" | grep -qiE '\\btest|\\bspec|\\bjest|\\bmocha'; then
if [ -f "jest.config.js" ] || [ -f "jest.config.ts" ]; then
CONTEXT="Project uses Jest for testing."
elif [ -f ".mocharc.json" ] || [ -f ".mocharc.js" ]; then
CONTEXT="Project uses Mocha for testing."
fi
fi
# Detect if user is asking about deployment
if echo "$PROMPT" | grep -qiE '\\bdeploy|\\bproduction|\\brelease'; then
if [ -f ".github/workflows/deploy.yml" ]; then
CONTEXT="Project has GitHub Actions deployment workflow."
elif [ -f "Dockerfile" ]; then
CONTEXT="Project uses Docker for deployment."
fi
fi
printf '{"cancel":false,"contextModification":"%s","errorMessage":""}' "$CONTEXT"
`
}
function getPreCompactTemplate(): string {
return `#!/bin/bash
#
# ============================================================================
# PreCompact Hook
#
# Executes before conversation context is compacted (to free up token space).
#
# Input: {
# taskId,
# preCompact: {
# conversationLength: number,
# estimatedTokens: number
# },
# ...
# Runs before the conversation context is compacted (to free up token space).
# ============================================================================
#
# EXAMPLE INPUT (JSON via stdin):
# {
# "clineVersion": "3.17.0",
# "hookName": "PreCompact",
# "taskId": "abc123",
# "timestamp": "1749484935515",
# "workspaceRoots": ["/path/to/project"],
# "userId": "user-123",
# "preCompact": {
# "contextSize": 180000,
# "messagesToCompact": 45,
# "compactionStrategy": "half"
# }
# }
# Output: { cancel: boolean, contextModification?: string, errorMessage?: string }
#
# Use cases:
# - Archive important conversation parts
# - Log compaction events
# - Add summary before context is lost
#
# OUTPUT: JSON with { cancel, contextModification, errorMessage }
# - contextModification can add a summary that persists after compaction
# ============================================================================
# Read JSON input from stdin
INPUT=$(cat)
# Parse input using jq (or fallback to basic parsing)
if command -v jq &> /dev/null; then
CONV_LENGTH=$(echo "$INPUT" | jq -r '.preCompact.conversationLength')
EST_TOKENS=$(echo "$INPUT" | jq -r '.preCompact.estimatedTokens')
# --- Parse JSON ---
if command -v jq >/dev/null 2>&1; then
CONTEXT_SIZE=$(echo "$INPUT" | jq -r '.preCompact.contextSize // empty')
MSG_COUNT=$(echo "$INPUT" | jq -r '.preCompact.messagesToCompact // empty')
STRATEGY=$(echo "$INPUT" | jq -r '.preCompact.compactionStrategy // empty')
TASK_ID=$(echo "$INPUT" | jq -r '.taskId // empty')
else
CONV_LENGTH="<length>"
EST_TOKENS="<tokens>"
CONTEXT_SIZE=$(echo "$INPUT" | grep -o '"contextSize"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
MSG_COUNT=$(echo "$INPUT" | grep -o '"messagesToCompact"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
STRATEGY=$(echo "$INPUT" | grep -o '"compactionStrategy"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
TASK_ID=$(echo "$INPUT" | grep -o '"taskId"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"taskId"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
fi
echo "[PreCompact] About to compact conversation (messages: $CONV_LENGTH, tokens: $EST_TOKENS)" >&2
# --- Example: Log compaction events ---
# LOG_FILE="\${HOME}/.cline/compaction-log.txt"
# mkdir -p "$(dirname "$LOG_FILE")"
# echo "[\$(date '+%Y-%m-%d %H:%M:%S')] Compacting $MSG_COUNT messages (strategy: $STRATEGY)" >> "$LOG_FILE"
# Return result
echo "{\"cancel\":false,\"contextModification\":\"\",\"errorMessage\":\"\"}"
# --- Example: Archive important context before compaction ---
# Uncomment to save a summary before context is lost:
# ARCHIVE_DIR="\${HOME}/.cline/archives"
# mkdir -p "$ARCHIVE_DIR"
# echo "$INPUT" > "$ARCHIVE_DIR/$TASK_ID-\$(date +%s).json"
# --- Example: Add reminder of what was being worked on ---
CONTEXT=""
# Check current git status to remind about work in progress
if [ -d ".git" ]; then
STAGED=$(git diff --cached --name-only 2>/dev/null | wc -l | tr -d ' ')
if [ "$STAGED" -gt 0 ]; then
CONTEXT="Note: $STAGED file(s) are staged for commit."
fi
fi
printf '{"cancel":false,"contextModification":"%s","errorMessage":""}' "$CONTEXT"
`
}
function getDefaultTemplate(hookName: string): string {
return `#!/bin/bash
#
# ============================================================================
# ${hookName} Hook
#
# Input: JSON via stdin
# Output: JSON to stdout
# ============================================================================
#
# INPUT: JSON via stdin (contains taskId, timestamp, hookName, and hook-specific data)
# OUTPUT: JSON to stdout with { cancel, contextModification, errorMessage }
#
# - cancel: boolean - set to true to cancel/block the operation
# - contextModification: string - text to add to the conversation context
# - errorMessage: string - error message to show if cancelled
# ============================================================================
# Read JSON input from stdin
INPUT=$(cat)
# Parse input using jq (or fallback to basic parsing)
if command -v jq &> /dev/null; then
TASK_ID=$(echo "$INPUT" | jq -r '.taskId')
# --- Parse JSON ---
if command -v jq >/dev/null 2>&1; then
TASK_ID=$(echo "$INPUT" | jq -r '.taskId // empty')
else
TASK_ID="<taskId>"
TASK_ID=$(echo "$INPUT" | grep -o '"taskId"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"taskId"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/')
fi
# Your hook logic here
echo "[${hookName}] Executed for task $TASK_ID" >&2
# --- Your hook logic here ---
# Add your custom logic below
# Return result
echo "{\"cancel\":false,\"contextModification\":\"\",\"errorMessage\":\"\"}"
# --- Output result ---
printf '{"cancel":false,"contextModification":"","errorMessage":""}'
`
}