Skip to content

Claude Code Complete Guide

Claude Code Hooks: Advanced Conditional Execution and Workflow Control

Claude Code Hooks is a powerful system that provides deterministic control over Claude Code's behavior. Let's explore advanced methods for executing commands only under specific conditions, enabling precise workflow control such as automatically performing git push operations only when creating articles.

Overview

Claude Code Hooks are user-defined shell commands that execute at various stages of Claude Code's lifecycle. They provide six main intervention points:

  1. UserPromptSubmit - When a prompt is submitted (before Claude processing)
  2. PreToolUse - Before tool execution
  3. PostToolUse - After tool execution
  4. Notification - When a notification occurs
  5. Stop - When Claude response ends
  6. SubagentStop - When a subagent terminates

Conditional Branching with Environment Variables

Available Environment Variables

Claude Code Hooks automatically set environment variables from the context of triggered events:

# Available only in PostToolUse
$CLAUDE_TOOL_OUTPUT     # Tool execution result

# Available in all Hooks
$CLAUDE_TOOL_NAME       # Name of the executed tool
$CLAUDE_FILE_PATHS      # Related file paths (space-separated)
$CLAUDE_NOTIFICATION    # Notification content (Notification event only)

Implementation Example: Conditional Branching Using Environment Variables

#!/usr/bin/env python3
import os
import sys
import json
import subprocess

def main():
    # Retrieve information from environment variables
    tool_name = os.environ.get('CLAUDE_TOOL_NAME', '')
    file_paths = os.environ.get('CLAUDE_FILE_PATHS', '')
    tool_output = os.environ.get('CLAUDE_TOOL_OUTPUT', '')

    # Conditional branching: Execute only when Write tool creates markdown files
    if tool_name == 'Write' and any(path.endswith('.md') for path in file_paths.split()):
        print("Markdown file created - triggering git operations")
        # Execute git operations
        subprocess.run(['git', 'add', '.'], check=True)
        subprocess.run(['git', 'commit', '-m', 'Auto-commit: New article created'], check=True)
        subprocess.run(['git', 'push'], check=True)

    sys.exit(0)

if __name__ == '__main__':
    main()

Hooks Triggered Only by Specific Tool Usage

Conditional Execution via Matcher Configuration

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|MultiEdit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/format_and_commit.py"
          }
        ]
      }
    ]
  }
}

Advanced Tool Detection Example

#!/usr/bin/env python3
import json
import sys
import subprocess
import os

def is_article_creation_tool(tool_name, tool_input):
    """Determine if the tool is related to article creation"""
    if tool_name == 'Write':
        file_path = tool_input.get('file_path', '')
        # Creating markdown files in docs directory
        return file_path.startswith('docs/') and file_path.endswith('.md')

    if tool_name == 'Edit':
        file_path = tool_input.get('file_path', '')
        # Updating mkdocs.yml (adding new navigation)
        return file_path.endswith('mkdocs.yml')

    return False

def main():
    try:
        # Read JSON input
        input_data = json.load(sys.stdin)

        tool_name = input_data.get('tool_name', '')
        tool_input = input_data.get('tool_input', {})

        # Execute only for article creation-related tools
        if is_article_creation_tool(tool_name, tool_input):
            print("Article creation detected - executing git workflow")

            # Execute git operations
            subprocess.run(['git', 'add', '.'], check=True)
            subprocess.run(['git', 'commit', '-m', 'feat: Add new article'], check=True)
            subprocess.run(['git', 'push'], check=True)

            print("Article published successfully")

        sys.exit(0)

    except Exception as e:
        print(f"Hook execution failed: {e}", file=sys.stderr)
        sys.exit(0)

if __name__ == '__main__':
    main()

Conditional Execution Based on File Patterns

Advanced File Patterns in TOML Configuration

[[hooks]]
event = "PostToolUse"
[hooks.matcher]
tool_name = "Write"
file_paths = ["docs/**/*.md", "!docs/drafts/**"]
command = "python3 .claude/hooks/article_publisher.py"

Advanced File Pattern Detection in Python

#!/usr/bin/env python3
import fnmatch
import os
import json
import sys
import subprocess
from pathlib import Path

def matches_article_pattern(file_path):
    """Determine if file path matches article file patterns"""
    path = Path(file_path)

    # Exclusion patterns
    exclude_patterns = [
        "docs/drafts/**",
        "docs/templates/**",
        "**/.git/**",
        "**/__pycache__/**"
    ]

    # Inclusion patterns
    include_patterns = [
        "docs/**/*.md",
        "docs/**/*.ja.md"
    ]

    # Exclusion check
    for pattern in exclude_patterns:
        if path.match(pattern):
            return False

    # Inclusion check
    for pattern in include_patterns:
        if path.match(pattern):
            return True

    return False

def main():
    try:
        input_data = json.load(sys.stdin)

        tool_name = input_data.get('tool_name', '')
        tool_input = input_data.get('tool_input', {})

        if tool_name == 'Write':
            file_path = tool_input.get('file_path', '')

            if matches_article_pattern(file_path):
                print(f"Article file pattern matched: {file_path}")

                # Validate file content
                if validate_article_content(file_path):
                    # Execute git operations
                    execute_git_workflow(file_path)
                else:
                    print("Article validation failed - skipping publication")

        sys.exit(0)

    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(0)

def validate_article_content(file_path):
    """Validate article content"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()

        # Basic article structure check
        if not content.strip():
            return False

        # Check for front matter or title
        if not (content.startswith('---') or content.startswith('# ')):
            return False

        # Minimum character count check
        if len(content) < 100:
            return False

        return True

    except Exception:
        return False

def execute_git_workflow(file_path):
    """Execute git workflow"""
    try:
        # Stage the file
        subprocess.run(['git', 'add', file_path], check=True)

        # Commit
        article_title = extract_article_title(file_path)
        commit_message = f"feat: Add new article - {article_title}"
        subprocess.run(['git', 'commit', '-m', commit_message], check=True)

        # Push
        subprocess.run(['git', 'push'], check=True)

        print(f"Successfully published: {file_path}")

    except subprocess.CalledProcessError as e:
        print(f"Git operation failed: {e}", file=sys.stderr)

def extract_article_title(file_path):
    """Extract article title"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()

        # Search for markdown title
        lines = content.split('\n')
        for line in lines:
            if line.startswith('# '):
                return line[2:].strip()

        # Fallback to filename
        return Path(file_path).stem

    except Exception:
        return "Unknown Article"

if __name__ == '__main__':
    main()

Precise Control with PostToolUse Hook

Advanced Control via JSON Control Flow

#!/usr/bin/env python3
import json
import sys
import subprocess
import os
from pathlib import Path

def main():
    try:
        input_data = json.load(sys.stdin)

        tool_name = input_data.get('tool_name', '')
        tool_input = input_data.get('tool_input', {})
        tool_response = input_data.get('tool_response', {})

        # Check conditions for article creation
        if should_publish_article(tool_name, tool_input, tool_response):
            result = publish_article(tool_input, tool_response)

            if result['success']:
                # Control on success
                output = {
                    "continue": True,
                    "suppressOutput": False
                }
                print(json.dumps(output))
                print(f"✅ Article published: {result['url']}")
            else:
                # Control on failure - feedback to Claude
                output = {
                    "decision": "block",
                    "reason": f"Article publication failed: {result['error']}"
                }
                print(json.dumps(output))
                sys.exit(2)

        sys.exit(0)

    except Exception as e:
        print(f"Hook error: {e}", file=sys.stderr)
        sys.exit(0)

def should_publish_article(tool_name, tool_input, tool_response):
    """Determine if article should be published"""
    if tool_name != 'Write':
        return False

    file_path = tool_input.get('file_path', '')

    # Check if markdown file in docs directory
    if not (file_path.startswith('docs/') and file_path.endswith('.md')):
        return False

    # Check if tool execution succeeded
    if not tool_response.get('success', False):
        return False

    # Check if not a draft file
    if 'draft' in file_path.lower():
        return False

    return True

def publish_article(tool_input, tool_response):
    """Execute article publication workflow"""
    try:
        file_path = tool_input.get('file_path', '')

        # File existence check
        if not Path(file_path).exists():
            return {"success": False, "error": "File not found"}

        # Validate article content
        if not validate_article_structure(file_path):
            return {"success": False, "error": "Invalid article structure"}

        # Git operations
        git_result = execute_git_operations(file_path)
        if not git_result['success']:
            return git_result

        # Generate publication URL
        url = generate_article_url(file_path)

        return {
            "success": True,
            "url": url,
            "commit": git_result['commit_hash']
        }

    except Exception as e:
        return {"success": False, "error": str(e)}

def validate_article_structure(file_path):
    """Validate article structure"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()

        # Basic check
        if len(content.strip()) < 100:
            return False

        # Title existence check
        if not (content.startswith('# ') or '# ' in content[:500]):
            return False

        # Check for specific keywords
        forbidden_keywords = ['TODO', 'FIXME', '[placeholder]']
        for keyword in forbidden_keywords:
            if keyword in content:
                return False

        return True

    except Exception:
        return False

def execute_git_operations(file_path):
    """Execute git operations"""
    try:
        # Stage the file
        subprocess.run(['git', 'add', file_path], check=True)

        # Check if there are existing commits
        result = subprocess.run(['git', 'diff', '--cached', '--quiet'], 
                              capture_output=True)
        if result.returncode == 0:
            return {"success": True, "commit_hash": "no-changes"}

        # Commit
        title = extract_title(file_path)
        commit_msg = f"feat: Add article - {title}"
        subprocess.run(['git', 'commit', '-m', commit_msg], check=True)

        # Get commit hash
        result = subprocess.run(['git', 'rev-parse', 'HEAD'], 
                              capture_output=True, text=True, check=True)
        commit_hash = result.stdout.strip()

        # Push
        subprocess.run(['git', 'push'], check=True)

        return {"success": True, "commit_hash": commit_hash}

    except subprocess.CalledProcessError as e:
        return {"success": False, "error": f"Git operation failed: {e}"}

def extract_title(file_path):
    """Extract article title"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()

        lines = content.split('\n')
        for line in lines:
            if line.startswith('# '):
                return line[2:].strip()

        return Path(file_path).stem

    except Exception:
        return "Unknown"

def generate_article_url(file_path):
    """Generate article URL"""
    # Relative path from docs directory
    relative_path = Path(file_path).relative_to('docs')

    # Create URL path by removing .md
    url_path = str(relative_path).replace('.md', '/')

    return f"https://smartscope.blog/{url_path}"

if __name__ == '__main__':
    main()

Precise Control via Exit Codes

Exit Code Types and Behavior

Exit CodeBehaviorDescription
0SuccessNormal execution, stdout displayed in transcript
2BlockImportant: stderr automatically fed back to Claude
OtherNon-blockingstderr displayed to user, execution continues

Hook-Specific Control Flow

#!/usr/bin/env python3
import sys
import json
import subprocess

def main():
    try:
        input_data = json.load(sys.stdin)

        # Detect and block dangerous commands
        if is_dangerous_command(input_data):
            print("BLOCKED: Dangerous command detected", file=sys.stderr)
            sys.exit(2)  # Automatically notify Claude of error

        # Detect and handle article creation
        if is_article_creation(input_data):
            result = handle_article_creation(input_data)

            if result['success']:
                # JSON control on success
                output = {
                    "continue": True,
                    "suppressOutput": False
                }
                print(json.dumps(output))
                print(f"✅ Article published: {result['url']}")
                sys.exit(0)
            else:
                # JSON control on failure
                output = {
                    "decision": "block",
                    "reason": f"Publication failed: {result['error']}"
                }
                print(json.dumps(output))
                sys.exit(2)

        sys.exit(0)

    except Exception as e:
        print(f"Hook execution error: {e}", file=sys.stderr)
        sys.exit(1)  # Non-blocking error

def is_dangerous_command(input_data):
    """Detect dangerous commands"""
    tool_name = input_data.get('tool_name', '')

    if tool_name == 'Bash':
        command = input_data.get('tool_input', {}).get('command', '')

        dangerous_patterns = [
            r'rm\s+.*-[rf]',
            r'sudo\s+rm',
            r'>\s*/etc/',
            r'chmod\s+777'
        ]

        import re
        for pattern in dangerous_patterns:
            if re.search(pattern, command):
                return True

    return False

def is_article_creation(input_data):
    """Detect article creation"""
    tool_name = input_data.get('tool_name', '')
    tool_input = input_data.get('tool_input', {})

    if tool_name == 'Write':
        file_path = tool_input.get('file_path', '')
        return file_path.startswith('docs/') and file_path.endswith('.md')

    return False

def handle_article_creation(input_data):
    """Handle article creation"""
    try:
        file_path = input_data.get('tool_input', {}).get('file_path', '')

        # Validate article
        if not validate_article(file_path):
            return {"success": False, "error": "Article validation failed"}

        # Git operations
        subprocess.run(['git', 'add', file_path], check=True)
        subprocess.run(['git', 'commit', '-m', f'feat: Add article {file_path}'], check=True)
        subprocess.run(['git', 'push'], check=True)

        url = f"https://smartscope.blog/{file_path.replace('docs/', '').replace('.md', '/')}"

        return {"success": True, "url": url}

    except Exception as e:
        return {"success": False, "error": str(e)}

def validate_article(file_path):
    """Validate article"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()

        # Basic validation
        if len(content.strip()) < 100:
            return False

        if not content.startswith('# '):
            return False

        return True

    except Exception:
        return False

if __name__ == '__main__':
    main()

Practical Hook Configuration Examples

.claude/settings.json

{
  "permissions": {
    "allow": [
      "Bash(git:*)",
      "Write",
      "Edit",
      "Read"
    ],
    "deny": []
  },
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/article_publisher.py"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/security_check.py"
          }
        ]
      }
    ]
  }
}

Complete Implementation: Git Push Only on Article Creation

Here is a complete implementation example that executes git push only during article creation:

#!/usr/bin/env python3
# .claude/hooks/article_publisher.py

import json
import sys
import subprocess
import os
from pathlib import Path
import re

def main():
    """Hook that executes git push only on article creation"""
    try:
        # Read JSON input
        input_data = json.load(sys.stdin)

        # Check article creation conditions
        if is_article_creation(input_data):
            result = publish_article(input_data)

            if result['success']:
                print(f"✅ Article published successfully: {result['url']}")
                # Control flow on success
                output = {
                    "continue": True,
                    "suppressOutput": False
                }
                print(json.dumps(output))
            else:
                # Feed back to Claude on failure
                output = {
                    "decision": "block",
                    "reason": f"Article publication failed: {result['error']}"
                }
                print(json.dumps(output))
                sys.exit(2)

        sys.exit(0)

    except Exception as e:
        print(f"Hook execution error: {e}", file=sys.stderr)
        sys.exit(0)

def is_article_creation(input_data):
    """Determine if this is article creation"""
    tool_name = input_data.get('tool_name', '')
    tool_input = input_data.get('tool_input', {})
    tool_response = input_data.get('tool_response', {})

    # Only target successful Write tool executions
    if tool_name != 'Write' or not tool_response.get('success', False):
        return False

    file_path = tool_input.get('file_path', '')

    # Article file conditions
    conditions = [
        file_path.startswith('docs/'),  # Inside docs directory
        file_path.endswith('.md'),      # Markdown file
        'draft' not in file_path.lower(), # Not a draft
        'template' not in file_path.lower() # Not a template
    ]

    return all(conditions)

def publish_article(input_data):
    """Article publication workflow"""
    try:
        file_path = input_data.get('tool_input', {}).get('file_path', '')

        # File existence check
        if not Path(file_path).exists():
            return {"success": False, "error": "File not found"}

        # Validate article content
        validation_result = validate_article_content(file_path)
        if not validation_result['valid']:
            return {"success": False, "error": validation_result['error']}

        # Execute git workflow
        git_result = execute_git_workflow(file_path)
        if not git_result['success']:
            return git_result

        # Generate publication URL
        url = generate_article_url(file_path)

        return {
            "success": True,
            "url": url,
            "commit": git_result['commit_hash']
        }

    except Exception as e:
        return {"success": False, "error": str(e)}

def validate_article_content(file_path):
    """Detailed validation of article content"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()

        # Basic length check
        if len(content.strip()) < 200:
            return {"valid": False, "error": "Article too short (minimum 200 characters)"}

        # Title existence check
        if not content.startswith('# '):
            return {"valid": False, "error": "Article title (line starting with #) required"}

        # Forbidden keyword check
        forbidden_keywords = ['TODO', 'FIXME', '[placeholder]', 'XXX']
        for keyword in forbidden_keywords:
            if keyword in content:
                return {"valid": False, "error": f"Incomplete marker '{keyword}' found"}

        # Additional checks for Japanese articles
        if has_japanese_content(content):
            if not validate_japanese_article(content):
                return {"valid": False, "error": "Japanese article format is not appropriate"}

        return {"valid": True, "error": None}

    except Exception as e:
        return {"valid": False, "error": f"File read error: {str(e)}"}

def has_japanese_content(content):
    """Check if Japanese content is present"""
    japanese_pattern = r'[ひらがなカタカナ漢字]'
    return bool(re.search(japanese_pattern, content))

def validate_japanese_article(content):
    """Format check for Japanese articles"""
    # Basic Japanese sentence structure check
    sentences = content.split('。')
    if len(sentences) < 3:
        return False

    # Heading structure check
    headers = re.findall(r'^#+\s+.+', content, re.MULTILINE)
    if len(headers) < 2:  # Title + at least one heading
        return False

    return True

def execute_git_workflow(file_path):
    """Execute git workflow"""
    try:
        # Check current git status
        result = subprocess.run(['git', 'status', '--porcelain'], 
                              capture_output=True, text=True, check=True)

        # Skip if no changes
        if not result.stdout.strip():
            return {"success": True, "commit_hash": "no-changes"}

        # Stage the file
        subprocess.run(['git', 'add', file_path], check=True)

        # Also commit mkdocs.yml if updated
        mkdocs_path = 'mkdocs.yml'
        if Path(mkdocs_path).exists():
            subprocess.run(['git', 'add', mkdocs_path], check=True)

        # Commit
        title = extract_article_title(file_path)
        commit_message = f"feat: Add new article - {title}"
        subprocess.run(['git', 'commit', '-m', commit_message], check=True)

        # Get commit hash
        result = subprocess.run(['git', 'rev-parse', 'HEAD'], 
                              capture_output=True, text=True, check=True)
        commit_hash = result.stdout.strip()

        # Push
        subprocess.run(['git', 'push'], check=True)

        return {"success": True, "commit_hash": commit_hash}

    except subprocess.CalledProcessError as e:
        return {"success": False, "error": f"Git operation error: {e}"}

def extract_article_title(file_path):
    """Extract article title"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()

        # Search for first line starting with #
        lines = content.split('\n')
        for line in lines:
            if line.startswith('# '):
                return line[2:].strip()

        # Use filename if title not found
        return Path(file_path).stem

    except Exception:
        return "Unknown Article"

def generate_article_url(file_path):
    """Generate article URL"""
    # Relative path from docs directory
    relative_path = Path(file_path).relative_to('docs')

    # Create URL path by removing .md
    url_path = str(relative_path).replace('.md', '/')

    return f"https://smartscope.blog/{url_path}"

if __name__ == '__main__':
    main()

Summary

Claude Code Hooks enable the following advanced conditional execution capabilities:

  1. Conditional branching via environment variables - Utilize $CLAUDE_TOOL_OUTPUT, $CLAUDE_FILE_PATHS, etc.
  2. Triggering on specific tools - Limit target tools using Matcher configuration
  3. File pattern-based execution - Processing targeted at article files only
  4. Exit Code and JSON control - Precise flow control and error handling
  5. PostToolUse Hook - Automated workflows after tool execution

By combining these features, you can build extremely precise and reliable automated workflows, such as executing git push only during article creation.

Note: Hooks are powerful features, but they automatically execute arbitrary shell commands, creating security risks. Perform thorough testing and validation during implementation.