██████╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗
██╔════╝ ██╔════╝ ██║ ██║ ██╔═══██╗ ██╔═══██╗ ██║ ██╔╝
██║ ██║ ███████║ ██║ ██║ ██║ ██║ █████╔╝
██║ ██║ ██╔══██║ ██║ ██║ ██║ ██║ ██╔═██╗
╚██████╗ ╚██████╗ ██║ ██║ ╚██████╔╝ ╚██████╔╝ ██║ ██╗
╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
A CLI tool for executing hooks at various stages of Claude Code operations.
Claude Code has a powerful hook system that allows executing custom commands at various stages of operation. However, writing hooks can become unwieldy for several reasons:
- Complex JSON configuration
- Hooks are configured in JSON format within settings, making them hard to read and maintain
- Repetitive jq processing
- When using multiple elements from input JSON, you need temporary files and repeated jq filters
- Single-line limitations
- JSON strings don't support multi-line formatting like YAML, leading to very long, hard-to-read command lines
For example, a simple Stop hook that sends notifications via ntfy becomes a complex one-liner:
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "transcript_path=$(jq -r '.transcript_path') && cat \"${transcript_path}\" | jq -s 'reverse | map(select(.type == \"assistant\" and .message.content[0].type == \"text\")) | .[0].message.content[0]' > /tmp/cc_ntfy.json && ntfy publish --markdown --title 'Claude Code' \"$(cat /tmp/cc_ntfy.json | jq -r '.text')\""
}
]
}
]
}
}cchook solves these problems by providing:
- YAML configuration
- Clean, readable multi-line configuration
- Template syntax
- Simple
{.field}syntax for accessing JSON data with full jq query support
- Simple
- Conditional logic
- Built-in conditions for common scenarios (file extensions, command patterns, etc.)
- Better maintainability
- Structured configuration that's easy to understand and modify
- YAML Configuration: Write clean, maintainable hook configurations
- Template Engine: Use
{.field}syntax with full jq query support - Conditional Execution: Execute actions based on file types, commands, or prompts
- Error Handling: Robust error handling for unknown condition types
- Dry-Run Mode: Test configurations before deployment
- Performance: Cached jq query compilation for efficient template processing
go install github.com/syou6162/cchook@latestgit clone https://github.com/syou6162/cchook
cd cchook
go build -o cchookAdd cchook to your Claude Code hook configuration in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "cchook -event PreToolUse"
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "cchook -event PostToolUse"
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "cchook -event SessionStart"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "cchook -event UserPromptSubmit"
}
]
}
]
}
}Create ~/.config/cchook/config.yaml with your desired hooks:
# Auto-format Go files after Write/Edit
PostToolUse:
- matcher: "Write|Edit"
conditions:
- type: file_extension
value: ".go"
actions:
- type: command
command: "gofmt -w {.tool_input.file_path}"
# Guide users to use better alternatives
PreToolUse:
- matcher: "Bash"
conditions:
- type: command_starts_with
value: "python"
actions:
- type: output
message: "pythonは使わず`uv`を代わりに使いましょう"
- matcher: "WebFetch"
conditions:
- type: url_starts_with
value: "https://github.com"
actions:
- type: output
message: "WebFetchではなく、`gh`コマンド経由で情報を取得しましょう"-event(required): Specify the event type (PreToolUse, PostToolUse, SessionStart, SessionEnd, etc.)-config: Path to configuration file (default:~/.config/cchook/config.yaml)-command: Override configuration with a single command (useful for dry-run testing)
By default, cchook looks for configuration files in the following order:
- Path specified by
-configflag $XDG_CONFIG_HOME/cchook/config.yaml(ifXDG_CONFIG_HOMEis set)~/.config/cchook/config.yaml(default fallback)
You can specify a custom configuration file path using the -config flag:
# Use custom config file
cchook -config /path/to/my-config.yaml -event PreToolUse
# Example: Development vs Production configs
cchook -config ~/.config/cchook/dev-config.yaml -event PostToolUse
cchook -config ~/.config/cchook/prod-config.yaml -event StopTest your configuration without making actual changes:
# Test with a simple echo command
echo '{"session_id":"test","hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":"test.go"}}' | \
cchook -event PreToolUse -command "echo 'Would process: {.tool_name} on {.tool_input.file_path}'"{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "cchook -config ~/.config/cchook/dev-config.yaml -event PreToolUse"
}
]
}
]
}
}Auto-format different file types:
PostToolUse:
- matcher: "Write|Edit"
conditions:
- type: file_extension
value: ".go"
actions:
- type: command
command: "gofmt -w {.tool_input.file_path}"
- matcher: "Write|Edit"
conditions:
- type: file_extension
value: ".py"
actions:
- type: command
command: "black {.tool_input.file_path}"Run pre-commit hooks automatically:
PostToolUse:
- matcher: "Write|Edit|MultiEdit"
conditions:
- type: file_exists
value: ".pre-commit-config.yaml"
actions:
- type: command
command: "pre-commit run --files {.tool_input.file_path}"Conditional processing based on project type:
PreToolUse:
- matcher: "Write|Edit"
conditions:
- type: file_extension
value: ".py"
- type: file_exists_recursive
value: "pyproject.toml"
actions:
- type: output
message: "📝 Python project detected with pyproject.toml"Pass full JSON input to external commands via stdin for safe handling of special characters:
PreToolUse:
- matcher: "Write|Edit"
conditions:
- type: file_extension
value: ".sql"
actions:
- type: command
command: "python validate_sql.py"
use_stdin: true
- matcher: "mcp__codex__codex"
actions:
- type: command
# use_stdin: true is required here because tool_input.prompt may contain
# newlines, quotes, and special characters that would break shell escaping
command: "jq -r .tool_input.prompt | python analyze_prompt.py"
use_stdin: trueBenefits of use_stdin: true:
- Safely handles newlines, quotes, backslashes, and other special characters
- Avoids shell escaping issues with complex data
- Works with multi-line SQL queries, code snippets, and markdown content
- Passes entire JSON to command for flexible processing with jq, python, etc.
Enable specific hooks based on the current working directory:
# Use special settings for a specific project
PreToolUse:
- matcher: "Write|Edit"
conditions:
- type: cwd_contains
value: "/work/important-project"
actions:
- type: command
command: "echo '⚠️ Important project - all changes are being logged' >> /tmp/audit.log"
# Prevent operations in system directories
PreToolUse:
- matcher: "Bash"
conditions:
- type: cwd_is
value: "/"
actions:
- type: output
message: "🚫 Operations in root directory are not allowed!"
exit_status: 1
# Use different formatters for different repositories
PostToolUse:
- matcher: "Write|Edit"
conditions:
- type: cwd_contains
value: "github.com/golang"
- type: file_extension
value: ".go"
actions:
- type: command
command: "gofmt -w {.tool_input.file_path}"Block dangerous commands:
PreToolUse:
- matcher: "Bash"
conditions:
- type: command_starts_with
value: "rm -rf"
actions:
- type: output
message: "🚫 Dangerous command blocked!"
# Protect Git-tracked files from accidental deletion/move
- matcher: "Bash"
conditions:
- type: git_tracked_file_operation
value: "rm|mv" # Check both rm and mv commands
actions:
- type: output
message: |
⚠️ Error: Attempting to operate on Git-tracked files
Use 'git rm' or 'git mv' instead for Git-tracked files
Command attempted: {.tool_input.command}Send completion notifications:
Stop:
- actions:
- type: command
command: >
cat '{.transcript_path}' |
jq -s 'reverse | map(select(.type == "assistant" and .message.content[0].type == "text")) | .[0].message.content[0].text' |
xargs -I {} ntfy publish --markdown --title 'Claude Code Complete' "{}"Initialize session with custom setup:
SessionStart:
- matcher: "startup"
actions:
- type: command
command: "echo 'Session {.session_id} started at $(date)' >> ~/claude-sessions.log"
- type: output
message: "🚀 Claude Code session initialized"
# Project-specific initialization
- matcher: "startup"
conditions:
- type: file_exists
value: "go.mod"
actions:
- type: output
message: "Go project detected - remember to run tests"
- matcher: "startup"
conditions:
- type: file_exists_recursive
value: "pyproject.toml"
actions:
- type: output
message: "Python project detected - using uv for package management"Guide users based on their prompts using regex patterns:
UserPromptSubmit:
- conditions:
- type: prompt_regex
value: "\\?$"
actions:
- type: output
message: "❓ ユーザーが質問しています。コードの変更などはせず、質問の回答だけに専念しましょう"
# Periodic reminders to use efficient tools
- conditions:
- type: every_n_prompts
value: "10" # Every 10 prompts
actions:
- type: output
message: |
💡 Tip: Consider using specialized tools for better efficiency:
- Use serena MCP for code search and modification
- Use ripgrep (rg) instead of grep for faster searchingPrevent operations when certain files or directories exist or don't exist:
PreToolUse:
# Prevent building when build directory already exists
- matcher: "Bash"
conditions:
- type: dir_exists
value: "build"
- type: command_starts_with
value: "make"
actions:
- type: output
message: "Build directory already exists. Run 'make clean' first."
exit_status: 1
# Warn when package-lock.json doesn't exist
- matcher: "Bash"
conditions:
- type: file_not_exists
value: "package-lock.json"
- type: command_starts_with
value: "npm install"
actions:
- type: output
message: "⚠️ Warning: package-lock.json not found. This may cause dependency issues."
# Create backup directory if it doesn't exist
- matcher: "Write|Edit"
conditions:
- type: dir_not_exists
value: "backups"
actions:
- type: command
command: "mkdir -p backups && echo 'Created backup directory'"
PostToolUse:
# Check for missing test files
- matcher: "Write"
conditions:
- type: file_extension
value: ".go"
- type: file_not_exists_recursive
value: "main_test.go"
actions:
- type: output
message: "Consider adding tests for {.tool_input.file_path}"PreToolUse- Before tool execution (can block with exit_status: 2)
PostToolUse- After tool execution
Stop- When Claude Code session ends
SubagentStop- When a subagent terminates
Notification- System notifications
PreCompact- Before conversation compaction
SessionStart- When Claude Code session starts
- Supports conditions like
file_existsandfile_exists_recursive
UserPromptSubmit- When user submits a prompt
matcher- Match tool name using pipe-separated patterns (e.g., "Write|Edit", "Bash", "WebFetch")
- Empty matcher matches all tools
- Uses the same syntax as Claude Code's built-in hook matcher field
All conditions return proper error messages for unknown condition types, ensuring clear feedback when misconfigured.
File Operations:
file_exists- Check if specified file exists
file_exists_recursive- Check if file exists recursively in directory tree
file_not_exists- Check if specified file does not exist
file_not_exists_recursive- Check if file does not exist anywhere in directory tree
Directory Operations:
dir_exists- Check if specified directory exists
dir_exists_recursive- Check if directory exists recursively in directory tree
dir_not_exists- Check if specified directory does not exist
dir_not_exists_recursive- Check if directory does not exist anywhere in directory tree
Working Directory:
cwd_is- Check if current working directory exactly matches the specified path
cwd_is_not- Check if current working directory does not match the specified path
cwd_contains- Check if current working directory contains the specified substring
cwd_not_contains- Check if current working directory does not contain the specified substring
- All common conditions, plus:
file_extension- Match file extension in
tool_input.file_path
- Match file extension in
command_contains- Match substring in
tool_input.command
- Match substring in
command_starts_with- Match command prefix
url_starts_with- Match URL prefix (WebFetch tool)
git_tracked_file_operation- Check if command (rm, mv, etc.) operates on Git-tracked files
- Value specifies commands to check (e.g.,
"rm","mv","rm|mv")
- All common conditions, plus:
prompt_regex- Match user prompt with regular expression
- Supports OR conditions:
"help|助けて|サポート" - Supports anchors:
"^prefix"(starts with),"suffix$"(ends with) - Supports complex patterns:
"^(DEBUG|INFO|WARN|ERROR):"
every_n_prompts- Trigger action every N user prompts in the session
- Counts user messages from transcript file
- Example:
value: "10"triggers on 10th, 20th, 30th... prompts
reason_is- Match the session end reason
- Values:
"clear","logout","prompt_input_exit","other" - Example:
value: "clear"matches when session is cleared
- Support all common conditions (file, directory, and working directory operations)
- Support all common conditions (file, directory, and working directory operations)
command- Execute shell command
use_stdin: true(optional)- Pass full JSON input to command's stdin instead of using shell interpolation
- Solves issues with special characters (quotes, backslashes, newlines) in data
- Safer than shell string interpolation for complex data
- Example:
jq -r .tool_input.contentto extract content from JSON via stdin
output- Print message
- Default
exit_status:- 0 for SessionStart, SessionEnd, UserPromptSubmit (non-blocking events)
- 2 for PreToolUse, PostToolUse, Stop, SubagentStop, Notification, PreCompact
- 0
- Success, allow execution, output to stdout
- 2
- Block execution (PreToolUse), output to stderr
- Claude will process the stderr message
- Other (1, 3, etc.)
- Non-blocking error, stderr shown to user
- Execution continues normally
Access JSON data using {.field} syntax with full jq query support:
- Simple fields
{.session_id},{.tool_name},{.hook_event_name}
- Nested fields
{.tool_input.file_path},{.tool_input.url}
- Complex queries
{.transcript_path | @base64},{.tool_input | keys}
- Entire object
{.}
YAML Multi-line Support:
>- Folded style (newlines become spaces)
|- Literal style (preserves formatting)
PostToolUse:
- matcher: "Write|Edit"
conditions:
- type: file_extension
value: ".py"
- type: file_exists
value: "pyproject.toml"
actions:
- type: command
command: "ruff format {.tool_input.file_path}"
- type: command
command: "ruff check --fix {.tool_input.file_path}"PostToolUse:
- matcher: "Write|Edit"
conditions:
- type: file_extension
value: ".go"
actions:
- type: command
command: "gofmt -w {.tool_input.file_path}"
- type: command
command: "go vet {.tool_input.file_path}"
- type: output
message: "✅ Go file formatted and vetted: {.tool_input.file_path}"Stop:
- actions:
- type: command
command: |
LAST_MSG=$(cat '{.transcript_path}' | jq -s 'reverse | map(select(.type == "assistant" and .message.content[0].type == "text")) | .[0].message.content[0].text' | head -c 100)
ntfy publish --markdown --title 'Claude Code Session Complete' --tags 'checkmark' "$LAST_MSG..."cchook receives JSON input from Claude Code hooks via stdin. For details on the JSON structure and available fields, see the Claude Code hook documentation.
# Run all tests
go test ./...
# Run with verbose output
go test -v ./...
# Run with coverage
go test -cover ./...
# Generate coverage report
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out# Build binary
go build -o cchook
# Install locally
go install# Using pre-commit hooks
pre-commit run --all-files
# Direct golangci-lint
golangci-lint runMIT