Thanks to visit codestin.com
Credit goes to github.com

Skip to content

A CLI tool that simplifies Claude Code's hook system by replacing complex JSON configurations with clean YAML syntax, template-based data access, and conditional logic

License

Notifications You must be signed in to change notification settings

syou6162/cchook

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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
  • 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

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}"

# 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:

  1. Path specified by -config flag
  2. $XDG_CONFIG_HOME/cchook/config.yaml (if XDG_CONFIG_HOME is set)
  3. ~/.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}"

  - 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"

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:

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' "{}"

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
  • Notification
    • System notifications
  • PreCompact
    • Before conversation compaction
  • SessionStart
    • When Claude Code session starts
    • Supports conditions like file_exists and file_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

PreToolUse & PostToolUse

  • All common conditions, plus:
  • file_extension
    • Match file extension in tool_input.file_path
  • command_contains
    • Match substring in tool_input.command
  • 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.content to 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

Exit Status Control

  • 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

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}"
      - type: command
        command: "ruff check --fix {.tool_input.file_path}"

Multi-Step Workflows

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}"

Complex Notifications

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..."

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

About

A CLI tool that simplifies Claude Code's hook system by replacing complex JSON configurations with clean YAML syntax, template-based data access, and conditional logic

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages