Главная / 🪝 Hooks

🪝 Hooks — автоматизация

Hooks — скрипты Node.js (CJS), которые запускаются автоматически до/после действий Claude. Они перехватывают опасные команды, форматируют код, защищают критические файлы и считают стоимость сессии.

📋 Обзор hooks системы

HookСобытиеМатчерДействие
dangerous-command-guardPreToolUseBashБлокирует DROP TABLE, migrate:fresh и др.
bash-mcp-guardPreToolUseBashПеренаправляет docker → mcp__docker__*
secret-scannerPreToolUseBashБлокирует утечку API ключей
critical-files-guardPreToolUseEdit|Write|MultiEditЗащищает Caddyfile, .env.prod и др.
auto-formatPostToolUseEdit|Write|MultiEditАвто-форматирование pint/ruff/eslint
cost-trackerStop.*Логирует стоимость сессии

Как hooks регистрируются в settings.json

{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "node .claude/helpers/dangerous-command-guard.cjs" }, { "type": "command", "command": "node .claude/helpers/bash-mcp-guard.cjs" }, { "type": "command", "command": "node .claude/helpers/secret-scanner.cjs" } ] }, { "matcher": "Edit|Write|MultiEdit", "hooks": [ { "type": "command", "command": "node .claude/helpers/critical-files-guard.cjs" } ] } ], "PostToolUse": [ { "matcher": "Edit|Write|MultiEdit", "hooks": [ { "type": "command", "command": "node .claude/helpers/auto-format.cjs" } ] } ], "Stop": [ { "matcher": ".*", "hooks": [ { "type": "command", "command": "node .claude/helpers/cost-tracker.cjs" } ] } ] } }
💡
Exit codes: exit 0 — OK, продолжить. exit 1 — предупреждение (stderr), но продолжить. exit 2 — БЛОК, Claude отменяет действие и видит сообщение из stderr.
🛑 dangerous-command-guard.cjs
PreToolUse
Главный защитник. Блокирует команды, которые могут уничтожить данные или нарушить систему. Два уровня: HARD BLOCK (exit 2) и WARN (stderr, exit 0).
Матчер: Bash
Файл: .claude/helpers/dangerous-command-guard.cjs
// dangerous-command-guard.cjs 'use strict'; const input = JSON.parse(require('fs').readFileSync(0, 'utf8')); const cmd = (input.tool_input?.command || '').toLowerCase(); // УРОВЕНЬ 1: HARD BLOCK — exit 2, Claude видит ошибку const hardBlocked = [ // Laravel 'artisan migrate:fresh', 'artisan db:wipe', 'artisan migrate:reset', // SQL 'drop table', 'drop database', 'truncate table', 'delete from', 'delete *', // Git 'git push --force', 'git push -f', 'git reset --hard', 'git checkout --', 'git restore', '--no-verify', // БД напрямую 'psql ', 'pg_dump', 'mysqldump', // Docker volume 'docker volume rm', 'docker system prune', // rm -rf 'rm -rf', 'rm -r /', ]; for (const pattern of hardBlocked) { if (cmd.includes(pattern)) { process.stderr.write(`[BLOCKED] Опасная команда: "${pattern}"\n`); process.stderr.write(`Используйте MCP-инструменты или выполните вручную с CONFIRM_CRITICAL=yes\n`); process.exit(2); } } // УРОВЕНЬ 2: WARN — предупреждение, но продолжаем const warned = [ 'artisan migrate', 'npm ci', 'composer install --no-dev' ]; for (const pattern of warned) { if (cmd.includes(pattern)) { process.stderr.write(`[WARN] Потенциально опасная команда: "${pattern}". Убедитесь что это намеренно.\n`); } } process.exit(0);
🐳 bash-mcp-guard.cjs
PreToolUse
Перехватывает прямые docker-команды и подсказывает правильный MCP-инструмент. Это нужно потому что docker не в PATH на Windows и Bash hook блокирует прямой вызов.
// bash-mcp-guard.cjs 'use strict'; const input = JSON.parse(require('fs').readFileSync(0, 'utf8')); const cmd = (input.tool_input?.command || ''); const dockerMappings = [ { pattern: /^docker exec/, mcp: 'mcp__docker__docker_exec' }, { pattern: /^docker logs/, mcp: 'mcp__docker__docker_container_logs' }, { pattern: /^docker ps/, mcp: 'mcp__docker__docker_list_containers' }, { pattern: /^docker compose ps/, mcp: 'mcp__docker__docker_compose_ps' }, { pattern: /^docker compose up/, mcp: 'mcp__docker__docker_compose_up' }, { pattern: /^docker compose down/, mcp: 'mcp__docker__docker_compose_down' }, { pattern: /^docker inspect/, mcp: 'mcp__docker__docker_inspect_container' }, { pattern: /^docker stats/, mcp: 'mcp__docker__docker_container_stats' }, { pattern: /^docker restart/, mcp: 'mcp__docker__docker_restart_container' }, ]; for (const { pattern, mcp } of dockerMappings) { if (pattern.test(cmd.trim())) { process.stderr.write( `[DOCKER→MCP] Используй MCP инструмент: ${mcp}\n` + `Прямые docker команды заблокированы на Windows Server. MCP работает через Docker SDK.\n` ); process.exit(2); } } process.exit(0);
📁 critical-files-guard.cjs
PreToolUse
Защищает критически важные файлы от случайного изменения. При попытке редактирования — блок с объяснением. Обходится через переменную CONFIRM_CRITICAL=yes.
// critical-files-guard.cjs 'use strict'; const input = JSON.parse(require('fs').readFileSync(0, 'utf8')); const filePath = input.tool_input?.file_path || input.tool_input?.path || ''; if (process.env.CONFIRM_CRITICAL === 'yes') process.exit(0); const criticalPatterns = [ // Reverse proxy /Caddyfile/, /proxy[\/\\].*\.json/, // Системные /.wslconfig/, /docker-compose\.production/, // БД миграции (осторожно) /database[\/\\]migrations[\/\\]/, // Auth middleware /Middleware[\/\\]Authenticate/, // Env файлы /.env\.production/, /.env\.prod/, ]; for (const pattern of criticalPatterns) { if (pattern.test(filePath)) { process.stderr.write( `[PROTECTED] Файл "${filePath}" защищён от автоматического изменения.\n` + `Для редактирования: set CONFIRM_CRITICAL=yes (PowerShell: $env:CONFIRM_CRITICAL="yes")\n` ); process.exit(2); } } process.exit(0);
🔑 secret-scanner.cjs
PreToolUse
Сканирует bash-команды на наличие API ключей, токенов и других секретов. Самое важное — блокирует git add .env.
// secret-scanner.cjs 'use strict'; const input = JSON.parse(require('fs').readFileSync(0, 'utf8')); const cmd = input.tool_input?.command || ''; const secretPatterns = [ { pattern: /sk-[a-zA-Z0-9]{48}/, name: 'OpenAI API key' }, { pattern: /sk-ant-[a-zA-Z0-9]/, name: 'Anthropic API key' }, { pattern: /AKIA[0-9A-Z]{16}/, name: 'AWS Access Key' }, { pattern: /[0-9]{10}:[a-zA-Z0-9_-]{35}/, name: 'Telegram Bot Token' }, { pattern: /Bearer\s+[a-zA-Z0-9._-]{20,}/, name: 'Bearer Token' }, { pattern: /password\s*=\s*[^\s]{8,}/i, name: 'Password in command' }, { pattern: /git add .*\.env/, name: '.env файл в git!', hard: true }, { pattern: /git add -A/, name: 'git add -A (может включить .env)' }, ]; for (const { pattern, name, hard } of secretPatterns) { if (pattern.test(cmd)) { process.stderr.write(`[SECRET] Возможная утечка: ${name}\n`); process.stderr.write(`Команда: ${cmd.substring(0, 100)}\n`); if (hard) process.exit(2); } } process.exit(0);
✨ auto-format.cjs
PostToolUse
Автоматически форматирует файл после каждого изменения. PHP → Pint, Python → Ruff, TypeScript/Vue → ESLint. Никогда не блокирует (exit 0 всегда).
// auto-format.cjs 'use strict'; const { execSync } = require('child_process'); const input = JSON.parse(require('fs').readFileSync(0, 'utf8')); const filePath = input.tool_input?.file_path || input.tool_input?.path || ''; function run(cmd) { try { execSync(cmd, { stdio: 'pipe' }); } catch (e) { /* format errors не критичны */ } } if (filePath.endsWith('.php')) { // PSR-12 + declare(strict_types=1) run(`./vendor/bin/pint "${filePath}"`); } else if (filePath.endsWith('.py')) { // PEP 8 + imports sort run(`ruff check --fix "${filePath}"`); run(`ruff format "${filePath}"`); } else if (['.ts', '.vue', '.js'].some(ext => filePath.endsWith(ext))) { // ESLint flat config + Prettier run(`npx eslint --fix "${filePath}"`); } process.exit(0); // PostToolUse hooks НИКОГДА не должны блокировать
💰 cost-tracker.cjs
Stop
Запускается в конце каждой сессии. Рассчитывает стоимость по использованным токенам, пишет в JSONL файл. Алерт при превышении дневного бюджета.
// cost-tracker.cjs 'use strict'; const fs = require('fs'); const path = require('path'); const input = JSON.parse(fs.readFileSync(0, 'utf8')); const PRICES = { 'claude-opus-4-7': { input: 15, output: 75 }, // $ per 1M tokens 'claude-sonnet-4-6': { input: 3, output: 15 }, 'claude-haiku-4-5': { input: 0.8, output: 4 }, }; const model = input.model || 'claude-sonnet-4-6'; const usage = input.usage || {}; const price = PRICES[model] || PRICES['claude-sonnet-4-6']; const inputCost = (usage.input_tokens || 0) / 1_000_000 * price.input; const outputCost = (usage.output_tokens || 0) / 1_000_000 * price.output; const totalCost = inputCost + outputCost; const logDir = path.join(require('os').homedir(), '.claude', 'logs'); const today = new Date().toISOString().slice(0, 7); const logFile = path.join(logDir, `cost-${today}.jsonl`); fs.mkdirSync(logDir, { recursive: true }); fs.appendFileSync(logFile, JSON.stringify({ timestamp: new Date().toISOString(), model, totalCost: totalCost.toFixed(4), inputTokens: usage.input_tokens, outputTokens: usage.output_tokens }) + '\n'); const threshold = parseFloat(process.env.COST_ALERT_THRESHOLD || '5'); if (totalCost > threshold) { process.stderr.write(`[COST ALERT] Сессия: $${totalCost.toFixed(2)} (порог: $${threshold})\n`); } process.exit(0);

📥 Формат входных данных hook

Каждый hook получает JSON через stdin. Структура зависит от типа события:

// PreToolUse (Bash) — input.tool_input.command { "tool_name": "Bash", "tool_input": { "command": "php artisan migrate" } } // PreToolUse (Edit/Write) — input.tool_input.file_path { "tool_name": "Edit", "tool_input": { "file_path": "/path/to/file.php", "old_string": "...", "new_string": "..." } } // Stop — содержит usage статистику токенов { "model": "claude-sonnet-4-6", "usage": { "input_tokens": 45000, "output_tokens": 8000 } }