Agent Hooks
Agent hooks are shell commands that run automatically at specific lifecycle events during an AI coding agent session. Unlike skills (which provide advisory guidance to the model) or MCP servers (which provide tools), hooks are deterministic — they run your code, not the model, and they execute with the full power of your shell.
Hooks are the enforcement layer of the agent stack. A skill might suggest that the agent run tests after making changes. A hook guarantees it. A CLAUDE.md file might ask the agent not to modify certain files. A hook blocks it with exit code 2.
danger
Hooks fire at specific points during the agent session lifecycle. Each event provides different context and supports different use cases.
| Event | When It Fires | Can Block? | Common Use Cases |
|---|---|---|---|
| PreToolUse | Before any tool call executes | ✅ Yes (exit code 2) | Block dangerous commands, validate file paths, check permissions |
| PostToolUse | After any tool call completes | ❌ No | Auto-format code, run linters, log tool usage |
| SessionStart | When a new agent session begins | ❌ No | Load project context, check environment, set up state |
| SessionEnd | When the agent session ends | ❌ No | Cleanup, generate summary reports, commit changes |
| UserPromptSubmit | When the user submits a message | ❌ No | Log user requests, inject context, validate input |
| Notification | When the agent sends a notification | ❌ No | Custom alerting, external integrations, status updates |
These three mechanisms control agent behavior in fundamentally different ways. Understanding the difference is critical for building reliable agent systems.
| Property | Skills | Rules (CLAUDE.md) | Hooks |
|---|---|---|---|
| Execution | Model reads and follows | Model reads and follows | Shell command runs (no model) |
| Deterministic | ❌ No — model may ignore | ❌ No — model may ignore | ✅ Yes — always runs |
| Can block | ❌ No | ❌ No | ✅ Yes (exit code 2) |
| Context cost | Uses context window tokens | Uses context window tokens | Zero context cost |
| Best for | Workflows, conventions, patterns | Project context, coding standards | Validation, formatting, security gates |
info
Hooks are configured in your project .claude/settings.json file. Each hook specifies a matcher (which tools trigger it) and a command to run.
| 1 | { |
| 2 | "hooks": { |
| 3 | "PreToolUse": [ |
| 4 | { |
| 5 | "matcher": "Write|Edit", |
| 6 | "hooks": [ |
| 7 | { |
| 8 | "type": "command", |
| 9 | "command": "python3 ~/.claude/validators/validate_file.py" |
| 10 | } |
| 11 | ] |
| 12 | }, |
| 13 | { |
| 14 | "matcher": "Bash", |
| 15 | "hooks": [ |
| 16 | { |
| 17 | "type": "command", |
| 18 | "command": "python3 ~/.claude/validators/block_dangerous.py" |
| 19 | } |
| 20 | ] |
| 21 | } |
| 22 | ], |
| 23 | "PostToolUse": [ |
| 24 | { |
| 25 | "matcher": "Write|Edit", |
| 26 | "hooks": [ |
| 27 | { |
| 28 | "type": "command", |
| 29 | "command": "npx prettier --write \"$CLAUDE_FILE_PATHS\"" |
| 30 | } |
| 31 | ] |
| 32 | } |
| 33 | ], |
| 34 | "SessionStart": [ |
| 35 | { |
| 36 | "hooks": [ |
| 37 | { |
| 38 | "type": "command", |
| 39 | "command": "echo \"Session started at $(date)\" >> .claude/session.log" |
| 40 | } |
| 41 | ] |
| 42 | } |
| 43 | ] |
| 44 | } |
| 45 | } |
note
Pattern 1: Auto-Format on Save
Run Prettier (or Black, rustfmt, gofmt) after every file write or edit. This ensures the codebase stays formatted regardless of what the agent generates.
| 1 | { |
| 2 | "PostToolUse": [{ |
| 3 | "matcher": "Write|Edit", |
| 4 | "hooks": [{ |
| 5 | "type": "command", |
| 6 | "command": "npx prettier --write --log-level error \"$CLAUDE_FILE_PATHS\" 2>/dev/null || true" |
| 7 | }] |
| 8 | }] |
| 9 | } |
Pattern 2: Block Secrets in Code
Scan every file write for hardcoded secrets (API keys, passwords, tokens) and block the write if any are found.
| 1 | #!/usr/bin/env python3 |
| 2 | """Block writes that contain hardcoded secrets.""" |
| 3 | import re |
| 4 | import sys |
| 5 | import json |
| 6 | import os |
| 7 | |
| 8 | SECRET_PATTERNS = [ |
| 9 | r"sk-[a-zA-Z0-9]{32,}", # OpenAI API key |
| 10 | r"sk-ant-[a-zA-Z0-9-]{32,}", # Anthropic API key |
| 11 | r"ghp_[a-zA-Z0-9]{36}", # GitHub PAT |
| 12 | r"AKIA[0-9A-Z]{16}", # AWS Access Key |
| 13 | r"-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----", |
| 14 | ] |
| 15 | |
| 16 | def check_file(filepath: str) -> list[str]: |
| 17 | try: |
| 18 | with open(filepath) as f: |
| 19 | content = f.read() |
| 20 | return [p for p in SECRET_PATTERNS if re.search(p, content)] |
| 21 | except Exception: |
| 22 | return [] |
| 23 | |
| 24 | # Read file paths from environment |
| 25 | files = os.environ.get("CLAUDE_FILE_PATHS", "").split(":") |
| 26 | violations = [] |
| 27 | |
| 28 | for f in files: |
| 29 | if f.strip(): |
| 30 | found = check_file(f.strip()) |
| 31 | if found: |
| 32 | violations.append(f"{f}: {', '.join(found)}") |
| 33 | |
| 34 | if violations: |
| 35 | print(json.dumps({ |
| 36 | "decision": "block", |
| 37 | "reason": f"Hardcoded secrets detected: {'; '.join(violations)}" |
| 38 | })) |
| 39 | sys.exit(2) # Exit code 2 = block the operation |
| 40 | else: |
| 41 | sys.exit(0) # Exit code 0 = allow |
Pattern 3: Block Dangerous Commands
Prevent the agent from running destructive shell commands like rm -rf, force pushes, or database drops.
| 1 | #!/usr/bin/env python3 |
| 2 | """Block dangerous shell commands.""" |
| 3 | import json |
| 4 | import sys |
| 5 | import os |
| 6 | |
| 7 | DANGEROUS_PATTERNS = [ |
| 8 | "rm -rf /", |
| 9 | "rm -rf ~", |
| 10 | "rm -rf .", |
| 11 | "git push --force", |
| 12 | "git push -f", |
| 13 | "DROP TABLE", |
| 14 | "DROP DATABASE", |
| 15 | "TRUNCATE", |
| 16 | "> /dev/sda", |
| 17 | "mkfs.", |
| 18 | "dd if=", |
| 19 | ] |
| 20 | |
| 21 | tool_input = json.loads(os.environ.get("CLAUDE_TOOL_INPUT", "{}")) |
| 22 | command = tool_input.get("command", "") |
| 23 | |
| 24 | for pattern in DANGEROUS_PATTERNS: |
| 25 | if pattern.lower() in command.lower(): |
| 26 | print(json.dumps({ |
| 27 | "decision": "block", |
| 28 | "reason": f"Dangerous command blocked: '{pattern}' detected in: {command}" |
| 29 | })) |
| 30 | sys.exit(2) |
| 31 | |
| 32 | sys.exit(0) |
Pattern 4: Run Tests After Code Changes
Automatically run the test suite after the agent writes or edits code files. This catches regressions immediately.
| 1 | { |
| 2 | "PostToolUse": [{ |
| 3 | "matcher": "Write|Edit", |
| 4 | "hooks": [{ |
| 5 | "type": "command", |
| 6 | "command": "if [[ \"$CLAUDE_FILE_PATHS\" == *.ts || \"$CLAUDE_FILE_PATHS\" == *.tsx ]]; then npx jest --passWithNoTests --silent 2>&1 | tail -5; fi" |
| 7 | }] |
| 8 | }] |
| 9 | } |
Only PreToolUse hooks can block operations. The exit code determines the outcome:
| Exit Code | Behavior | Use Case |
|---|---|---|
| 0 | Allow the operation to proceed | Validation passed, everything is fine |
| 1 | Allow with a warning message to the model | Non-critical issue — agent sees the warning and can adjust |
| 2 | Block the operation entirely | Security violation, dangerous operation, policy breach |
warning
Hooks are your primary defense against agent misuse — both accidental and adversarial. A well-designed hook system provides defense in depth.
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.