A community-driven registry for the Claude Code ecosystem. Not affiliated with Anthropic.
Are you the author? Sign in to claim
Replace complex JSON hook config with clean YAML syntax and conditional logic
██████╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗
██╔════╝ ██╔════╝ ██║ ██║ ██╔═══██╗ ██╔═══██╗ ██║ ██╔╝
██║ ██║ ███████║ ██║ ██║ ██║ ██║ █████╔╝
██║ ██║ ██╔══██║ ██║ ██║ ██║ ██║ ██╔═██╗
╚██████╗ ╚██████╗ ██║ ██║ ╚██████╔╝ ╚██████╔╝ ██║ ██╗
╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
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:
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:
{.field} syntax for accessing JSON data with full jq query support{.field} syntax with full jq query supportgo install github.com/syou6162/cchook@latest
git clone https://github.com/syou6162/cchook
cd cchook
go build -o cchook
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"
}
]
}
]
}
}
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`コマンド経由で情報を取得しましょう"
-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:
-config flag$XDG_CONFIG_HOME/cchook/config.yaml (if XDG_CONFIG_HOME is 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 Stop
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}'"
{
"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}"
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"
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:
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 (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"}'
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 searching
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}"
PreToolUse
PostToolUse
Stop
SubagentStop
SubagentStart
Notification
PreCompact
SessionStart
file_exists and file_exists_recursiveUserPromptSubmit
matcher
All conditions return proper error messages for unknown condition types, ensuring clear feedback when misconfigured.
File Operations:
file_exists
file_exists_recursive
file_not_exists
file_not_exists_recursive
Directory Operations:
dir_exists
dir_exists_recursive
dir_not_exists
dir_not_exists_recursive
Working Directory:
cwd_is
cwd_is_not
cwd_contains
cwd_not_contains
Permission Mode:
permission_mode_is
file_extension
tool_input.file_pathcommand_contains
tool_input.commandcommand_starts_with
url_starts_with
git_tracked_file_operation
"rm", "mv", "rm|mv")prompt_regex
"help|助けて|サポート""^prefix" (starts with), "suffix$" (ends with)"^(DEBUG|INFO|WARN|ERROR):"every_n_prompts
value: "10" triggers on 10th, 20th, 30th... promptsreason_is
"clear", "logout", "prompt_input_exit", "other"value: "clear" matches when session is clearedcommand
use_stdin: true (optional)
jq -r .tool_input.content to extract content from JSON via stdinoutput_format: text (optional)
gofmt, black, pre-commit run that output plain textoutput
exit_status:
JSON Output Events (SessionStart, UserPromptSubmit, PreToolUse, Stop, SubagentStop, SubagentStart, PostToolUse, PreCompact, SessionEnd, Notification):
decision, permissionDecision, etc.)Legacy Exit Code Events (Notification only):
Migration Note (Stop):
exit_status: 0 (allow) or exit_status: 2 (block, default)decision field: omit for allow, "block" for denyexit_status field is ignored in JSON mode (stderr warning emitted)Migration Note (PreCompact):
exit_status: 2)decision field)continue: true (compaction cannot be blocked)exit_status field is ignored in JSON mode (stderr warning emitted)matcher: "manual" or "auto" to filter by trigger typeAccess JSON data using {.field} syntax with full jq query support:
{.session_id}, {.tool_name}, {.hook_event_name}{.tool_input.file_path}, {.tool_input.url}{.transcript_path | @base64}, {.tool_input | keys}{.}YAML Multi-line Support:
>
|
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
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}"
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"
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 run
MIT
Give Claude Code memory that evolves with your codebase via hooks and LLM-compiled knowledge
Security hooks with SSRF protection, MCP compression, and OpenTelemetry tracing
Context management with hooks for state via ledgers, MCP without context pollution
An LLM council that reviews your coding agent's every move for quality assurance
Community Package
@syou6162 on GitHub