Skip to content

Creating Custom Hooks

sem edited this page Jul 4, 2025 · 1 revision

Creating Custom Hooks

Learn how to create your own custom hooks for Claude Hooks Manager.

Overview

Custom hooks allow you to extend Claude Hooks Manager with project-specific automation. You can create hooks in various languages and integrate them seamlessly with the existing hook system.

Hook Structure

Basic Hook Anatomy

Every custom hook consists of:

  1. Hook Definition - Metadata about the hook
  2. Hook Script - The executable code
  3. Configuration Schema - Defines configurable options

Directory Structure

.claude-hooks/
├── custom/
│   ├── my-custom-hook/
│   │   ├── index.js
│   │   ├── hook.json
│   │   └── README.md
│   └── another-hook/
│       ├── run.sh
│       └── hook.json
└── config.json

Creating Your First Custom Hook

Step 1: Create Hook Directory

mkdir -p .claude-hooks/custom/my-custom-hook
cd .claude-hooks/custom/my-custom-hook

Step 2: Define Hook Metadata

Create hook.json:

{
  "name": "my-custom-hook",
  "version": "1.0.0",
  "description": "A custom hook that validates TODO comments",
  "type": "pre-commit",
  "executable": "index.js",
  "configSchema": {
    "maxTodos": {
      "type": "number",
      "default": 10,
      "description": "Maximum allowed TODO comments"
    },
    "requireAssignee": {
      "type": "boolean",
      "default": false,
      "description": "Require assignee in TODO comments"
    }
  }
}

Step 3: Write Hook Script

Create index.js:

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

// Get configuration
const config = JSON.parse(process.env.CLAUDE_HOOK_CONFIG || '{}');
const stagedFiles = process.env.CLAUDE_STAGED_FILES?.split('\n') || [];

// Hook logic
let todoCount = 0;
let errors = [];

for (const file of stagedFiles) {
  if (!file || !file.endsWith('.js')) continue;
  
  const content = fs.readFileSync(file, 'utf8');
  const lines = content.split('\n');
  
  lines.forEach((line, index) => {
    if (line.includes('TODO')) {
      todoCount++;
      
      if (config.requireAssignee && !line.match(/TODO\s*\(.+\)/)) {
        errors.push(`${file}:${index + 1} - TODO missing assignee`);
      }
    }
  });
}

// Check limits
if (todoCount > config.maxTodos) {
  console.error(`Error: Too many TODOs (${todoCount}/${config.maxTodos})`);
  process.exit(1);
}

if (errors.length > 0) {
  console.error('TODO validation errors:');
  errors.forEach(err => console.error(`  ${err}`));
  process.exit(1);
}

console.log(`✓ TODO check passed (${todoCount} TODOs found)`);
process.exit(0);

Step 4: Make Script Executable

chmod +x index.js

Step 5: Register the Hook

claude-hooks register my-custom-hook
claude-hooks install my-custom-hook

Hook Types and Environment Variables

Available Hook Types

  • pre-commit - Before commit is created
  • commit-msg - Validate/modify commit message
  • pre-push - Before pushing to remote
  • post-commit - After commit is created
  • pre-rebase - Before rebase operation

Environment Variables

All hooks receive these environment variables:

Variable Description Example
CLAUDE_HOOK_TYPE The hook type being run pre-commit
CLAUDE_HOOK_CONFIG JSON string of hook configuration {"maxTodos": 10}
CLAUDE_STAGED_FILES Newline-separated list of staged files src/index.js\nsrc/utils.js
CLAUDE_COMMIT_MSG_FILE Path to commit message file (commit-msg only) .git/COMMIT_EDITMSG
CLAUDE_PROJECT_ROOT Absolute path to project root /home/user/project

Advanced Hook Examples

Shell Script Hook

Create a hook that checks for sensitive data:

check-secrets/hook.json:

{
  "name": "check-secrets",
  "version": "1.0.0",
  "description": "Checks for accidentally committed secrets",
  "type": "pre-commit",
  "executable": "check.sh"
}

check-secrets/check.sh:

#!/bin/bash

# Patterns to check
patterns=(
  "password\s*=\s*[\"'][^\"']+[\"']"
  "api[_-]?key\s*=\s*[\"'][^\"']+[\"']"
  "secret\s*=\s*[\"'][^\"']+[\"']"
  "private[_-]?key"
)

# Check staged files
exit_code=0
while IFS= read -r file; do
  if [[ -z "$file" ]]; then
    continue
  fi
  
  for pattern in "${patterns[@]}"; do
    if grep -qiE "$pattern" "$file"; then
      echo "⚠️  Potential secret found in $file"
      echo "   Pattern: $pattern"
      exit_code=1
    fi
  done
done <<< "$CLAUDE_STAGED_FILES"

exit $exit_code

Python Hook

Create a hook that validates Python imports:

validate-imports/hook.json:

{
  "name": "validate-imports",
  "version": "1.0.0",
  "description": "Validates Python import statements",
  "type": "pre-commit",
  "executable": "validate.py",
  "configSchema": {
    "allowedImports": {
      "type": "array",
      "default": [],
      "description": "List of allowed import patterns"
    }
  }
}

validate-imports/validate.py:

#!/usr/bin/env python3

import os
import re
import json
import sys

# Get configuration
config = json.loads(os.environ.get('CLAUDE_HOOK_CONFIG', '{}'))
staged_files = os.environ.get('CLAUDE_STAGED_FILES', '').split('\n')

# Check Python files
errors = []
for file_path in staged_files:
    if not file_path.endswith('.py'):
        continue
    
    with open(file_path, 'r') as f:
        for line_num, line in enumerate(f, 1):
            if line.strip().startswith('import ') or 'from ' in line:
                # Check against allowed patterns
                allowed = False
                for pattern in config.get('allowedImports', []):
                    if re.match(pattern, line.strip()):
                        allowed = True
                        break
                
                if not allowed and config.get('allowedImports'):
                    errors.append(f"{file_path}:{line_num} - Unauthorized import: {line.strip()}")

if errors:
    print("Import validation errors:")
    for error in errors:
        print(f"  {error}")
    sys.exit(1)

print("✓ Import validation passed")
sys.exit(0)

Hook Communication

Inter-hook Communication

Hooks can communicate through shared state files:

// Write state
const state = { filesProcessed: 10, errors: [] };
fs.writeFileSync('.claude-hooks/.state/my-hook.json', JSON.stringify(state));

// Read state from another hook
const previousState = JSON.parse(
  fs.readFileSync('.claude-hooks/.state/previous-hook.json', 'utf8')
);

Hook Chaining

Define dependencies in hook.json:

{
  "name": "my-dependent-hook",
  "dependsOn": ["format-check", "lint-check"],
  "runAfter": ["test-check"]
}

Testing Custom Hooks

Unit Testing

Create test.js for your hook:

const { exec } = require('child_process');
const path = require('path');

describe('my-custom-hook', () => {
  it('should pass with valid files', (done) => {
    process.env.CLAUDE_STAGED_FILES = 'test/valid.js';
    process.env.CLAUDE_HOOK_CONFIG = JSON.stringify({ maxTodos: 10 });
    
    exec('node index.js', (error, stdout, stderr) => {
      expect(error).toBeNull();
      expect(stdout).toContain('✓ TODO check passed');
      done();
    });
  });
});

Integration Testing

Test with Claude Hooks Manager:

# Test run without committing
claude-hooks run my-custom-hook --test

# Test with specific files
claude-hooks run my-custom-hook --files "src/index.js,src/utils.js"

# Test with custom config
claude-hooks run my-custom-hook --config '{"maxTodos": 5}'

Best Practices

1. Performance

  • Process files in parallel when possible
  • Cache results for expensive operations
  • Exit early on first error (unless collecting all errors)

2. Error Handling

try {
  // Hook logic
} catch (error) {
  console.error(`Hook error: ${error.message}`);
  // Always exit with non-zero on error
  process.exit(1);
}

3. User-Friendly Output

// Use colors and symbols
console.log('\x1b[32m✓\x1b[0m All checks passed');
console.error('\x1b[31m✗\x1b[0m Validation failed');

// Provide actionable feedback
console.error('To fix: run "npm run format"');

4. Configuration Validation

// Validate configuration
const schema = require('./config-schema.json');
const valid = validateConfig(config, schema);

if (!valid) {
  console.error('Invalid configuration:', validation.errors);
  process.exit(1);
}

Publishing Custom Hooks

Package Structure

my-awesome-hook/
├── package.json
├── index.js
├── hook.json
├── README.md
├── LICENSE
└── test/
    └── test.js

Publishing to npm

  1. Create package.json:
{
  "name": "claude-hook-awesome",
  "version": "1.0.0",
  "description": "An awesome hook for Claude Hooks Manager",
  "main": "index.js",
  "keywords": ["claude-hooks", "git-hooks"],
  "files": ["index.js", "hook.json", "README.md"],
  "engines": {
    "node": ">=14.0.0"
  }
}
  1. Publish:
npm publish
  1. Users can install:
npm install -g claude-hook-awesome
claude-hooks register claude-hook-awesome

Troubleshooting Custom Hooks

Common Issues

  1. Hook not executing

    • Check file permissions: chmod +x your-script
    • Verify hook.json syntax
    • Check registration: claude-hooks list --custom
  2. Environment variables not available

    • Ensure using latest Claude Hooks Manager
    • Check variable names (case-sensitive)
  3. Hook timing out

    • Add progress output for long operations
    • Consider splitting into multiple hooks
    • Adjust timeout in configuration

Debug Mode

Enable debug output:

const DEBUG = process.env.CLAUDE_HOOK_DEBUG === 'true';

if (DEBUG) {
  console.log('Config:', config);
  console.log('Files:', stagedFiles);
}

Run with debug:

CLAUDE_HOOK_DEBUG=true git commit -m "test"

← Hooks Reference | Home | Configuration Guide →

Clone this wiki locally