-
Couldn't load subscription status.
- Fork 4
Creating Custom Hooks
Learn how to create your own custom hooks for Claude Hooks Manager.
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.
Every custom hook consists of:
- Hook Definition - Metadata about the hook
- Hook Script - The executable code
- Configuration Schema - Defines configurable options
.claude-hooks/
├── custom/
│ ├── my-custom-hook/
│ │ ├── index.js
│ │ ├── hook.json
│ │ └── README.md
│ └── another-hook/
│ ├── run.sh
│ └── hook.json
└── config.json
mkdir -p .claude-hooks/custom/my-custom-hook
cd .claude-hooks/custom/my-custom-hookCreate 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"
}
}
}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);chmod +x index.jsclaude-hooks register my-custom-hook
claude-hooks install my-custom-hook-
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
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 |
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_codeCreate 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)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')
);Define dependencies in hook.json:
{
"name": "my-dependent-hook",
"dependsOn": ["format-check", "lint-check"],
"runAfter": ["test-check"]
}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();
});
});
});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}'- Process files in parallel when possible
- Cache results for expensive operations
- Exit early on first error (unless collecting all errors)
try {
// Hook logic
} catch (error) {
console.error(`Hook error: ${error.message}`);
// Always exit with non-zero on error
process.exit(1);
}// 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"');// Validate configuration
const schema = require('./config-schema.json');
const valid = validateConfig(config, schema);
if (!valid) {
console.error('Invalid configuration:', validation.errors);
process.exit(1);
}my-awesome-hook/
├── package.json
├── index.js
├── hook.json
├── README.md
├── LICENSE
└── test/
└── test.js
- 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"
}
}- Publish:
npm publish- Users can install:
npm install -g claude-hook-awesome
claude-hooks register claude-hook-awesome-
Hook not executing
- Check file permissions:
chmod +x your-script - Verify hook.json syntax
- Check registration:
claude-hooks list --custom
- Check file permissions:
-
Environment variables not available
- Ensure using latest Claude Hooks Manager
- Check variable names (case-sensitive)
-
Hook timing out
- Add progress output for long operations
- Consider splitting into multiple hooks
- Adjust timeout in configuration
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"