README
¶
cchook
██████╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗
██╔════╝ ██╔════╝ ██║ ██║ ██╔═══██╗ ██╔═══██╗ ██║ ██╔╝
██║ ██║ ███████║ ██║ ██║ ██║ ██║ █████╔╝
██║ ██║ ██╔══██║ ██║ ██║ ██║ ██║ ██╔═██╗
╚██████╗ ╚██████╗ ██║ ██║ ╚██████╔╝ ╚██████╔╝ ██║ ██╗
╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
A CLI tool for executing hooks at various stages of Claude Code operations.
Background & Motivation
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
Features
- 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
- Extended Hook Support: Full support for Claude Code hooks specification including:
- SessionStart input fields (agent_type, model)
- PreToolUse additional context output
- Notification matcher filtering and extended input fields
- PostToolUse MCP tool output replacement
- SessionEnd bypass_permissions_disabled reason
Installation
go install github.com/syou6162/cchook@latest
Building from Source
git clone https://github.com/syou6162/cchook
cd cchook
go build -o cchook
Quick Start
1. Configure Claude Code Hooks
Add 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"
}
]
}
]
}
}
2. Create Configuration File
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}"
output_format: text # gofmt outputs plain text, not JSON
# 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`コマンド経由で情報を取得しましょう"
CLI Options
Flags
-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)
Configuration File Path
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)
Using Custom Configuration File
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 Stop
Dry-Run Testing
Test 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}'"
Example Claude Code Hook with Custom Config
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "cchook -config ~/.config/cchook/dev-config.yaml -event PreToolUse"
}
]
}
]
}
}
Configuration Examples
File Processing
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}"
output_format: text # gofmt outputs plain text, not JSON
- matcher: "Write|Edit"
conditions:
- type: file_extension
value: ".py"
actions:
- type: command
command: "black {.tool_input.file_path}"
output_format: text # black outputs plain text, not JSON
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}"
output_format: text # pre-commit outputs plain text, not JSON
Warn about sensitive file modifications:
PostToolUse:
- matcher: "Write|Edit"
conditions:
- type: file_extension
value: ".env"
actions:
- type: output
message: "Consider adding .env to .gitignore"
decision: "block"
reason: "Sensitive file modified - verify .gitignore configuration"
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"
Complex Data Handling with use_stdin
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: true
Benefits 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.
Working Directory Based Hooks
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}"
Command Safety
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}
Notifications
Send completion notifications (JSON output):
Stop:
- actions:
- type: command
command: |
# Extract last assistant message
LAST_MSG=$(cat '{.transcript_path}' | jq -s 'reverse | map(select(.type == "assistant" and .message.content[0].type == "text")) | .[0].message.content[0].text')
ntfy publish --markdown --title 'Claude Code Complete' "$LAST_MSG"
# Return JSON to allow stop
echo '{"continue": true, "systemMessage": "Notification sent"}'
Session Management
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"
User Prompt Filtering
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 searching
Directory and File Guards
Prevent 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}"
Configuration Reference
Event Types
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
SubagentStart- When a subagent starts (can filter by agent type with matcher)
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
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
Conditions
All conditions return proper error messages for unknown condition types, ensuring clear feedback when misconfigured.
Common Conditions (All Events)
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
Permission Mode:
permission_mode_is- Check if the current permission mode exactly matches the specified value (e.g., "default", "plan", "acceptEdits", "dontAsk", "bypassPermissions")
PreToolUse & PostToolUse
- 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")
UserPromptSubmit
- 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
SessionEnd Event
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)
Other Events (SessionStart, Stop, Notification, SubagentStop, PreCompact)
- Support all common conditions (file, directory, and working directory operations)
Actions
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_format: text(optional)- Treat command's stdout as plain text instead of JSON
- Without this, non-JSON output causes fail-safe block/deny behavior
- Use for standard CLI tools like
gofmt,black,pre-commit runthat output plain text - Non-zero exit code still triggers fail-safe behavior regardless of this setting
- Supported events: PreToolUse, PostToolUse, SessionStart, SessionEnd
output- Print message
- Default
exit_status:- 0 for SessionStart, UserPromptSubmit (non-blocking events)
- 2 for Notification (legacy)
- Note: Most events use JSON output (exit_status ignored). See "JSON Output Events" below.
Exit Status Control
JSON Output Events (SessionStart, UserPromptSubmit, PreToolUse, Stop, SubagentStop, SubagentStart, PostToolUse, PreCompact, SessionEnd, Notification):
- Always exit with code 0
- Control behavior via JSON fields (
decision,permissionDecision, etc.) - Errors logged to stderr as warnings
- See CLAUDE.md for detailed JSON output format
Legacy Exit Code Events (Notification only):
- 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
Migration Note (Stop):
- Prior to JSON support, Stop used
exit_status: 0(allow) orexit_status: 2(block, default) - After JSON migration, use
decisionfield: omit for allow,"block"for deny exit_statusfield is ignored in JSON mode (stderr warning emitted)
Migration Note (PreCompact):
- Prior to JSON support, PreCompact used exit codes (default
exit_status: 2) - After JSON migration, PreCompact uses Common JSON Fields only (no
decisionfield) - PreCompact always returns
continue: true(compaction cannot be blocked) exit_statusfield is ignored in JSON mode (stderr warning emitted)- Matcher support added:
matcher: "manual"or"auto"to filter by trigger type
Template Syntax
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)
Advanced Examples
Conditional File Processing
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}"
output_format: text
- type: command
command: "ruff check --fix {.tool_input.file_path}"
output_format: text
Multi-Step Workflows
PostToolUse:
- matcher: "Write|Edit"
conditions:
- type: file_extension
value: ".go"
actions:
- type: command
command: "gofmt -w {.tool_input.file_path}"
output_format: text # gofmt outputs plain text, not JSON
- type: command
command: "go vet {.tool_input.file_path}"
output_format: text # go vet outputs plain text, not JSON
- type: output
message: "✅ Go file formatted and vetted: {.tool_input.file_path}"
Session End Notifications (JSON Output)
Stop:
- actions:
- type: command
command: |
# Extract last assistant message and send notification
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..."
# Return JSON to allow stop (decision field omitted)
echo '{"continue": true, "systemMessage": "Notification sent"}'
Blocking Stop Example:
Stop:
- conditions:
- type: cwd_contains
value: "/critical-project"
actions:
- type: output
message: "Cannot stop in critical project directory"
decision: "block"
reason: "Stopping may lose important work context"
Input Format
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.
Development
Testing
# 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
Building
# Build binary
go build -o cchook
# Install locally
go install
Linting
# Using pre-commit hooks
pre-commit run --all-files
# Direct golangci-lint
golangci-lint run
License
MIT
Documentation
¶
There is no documentation for this package.