Claude Code's Hooks system lets you run scripts automatically before or after every tool call — code writes, bash commands, file edits. Configure them once, and every interaction gets automatic quality enforcement.
What Hooks Can Do
-
Before bash execution → Block dangerous commands (
rm -rf /,DROP DATABASE) - After file writes → Auto-format with Prettier/Ruff/gofmt
- After file writes → Scan for leaked API keys
- After source changes → Run related unit tests automatically
Hook Configuration
Add hooks to .claude/settings.json (project-level) or ~/.claude/settings.json (global):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "python .claude/hooks/guard.py"
}]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "python .claude/hooks/format.py",
"timeout": 30
}]
}
]
}
}
Exit Code Protocol
For PreToolUse hooks:
| Exit Code | Meaning |
|---|---|
| 0 | Allow — tool executes normally |
| 2 | Block — tool execution is cancelled |
| Other | Warning — logged, but execution continues |
Only PreToolUse can block. PostToolUse exit codes are ignored.
Guard Script: Block Destructive Commands
# .claude/hooks/guard.py
import json, sys
data = json.load(sys.stdin)
command = data.get("tool_input", {}).get("command", "")
BLOCKED = [
"rm -rf /",
"rm -rf ~",
"DROP DATABASE",
"git push --force origin main",
"git push --force origin master",
]
for pattern in BLOCKED:
if pattern in command:
print(f"[BLOCKED] {pattern}", file=sys.stderr)
sys.exit(2)
sys.exit(0)
Auto-Format Script: Language-Aware Formatting
# .claude/hooks/format.py
import json, subprocess, sys
from pathlib import Path
data = json.load(sys.stdin)
file_path = data.get("tool_input", {}).get("file_path", "")
if not file_path:
sys.exit(0)
p = Path(file_path)
FORMATTERS = {
".py": ["ruff", "format", "--quiet"],
".ts": ["npx", "prettier", "--write"],
".tsx": ["npx", "prettier", "--write"],
".js": ["npx", "prettier", "--write"],
".go": ["gofmt", "-w"],
".rs": ["rustfmt"],
}
fmt = FORMATTERS.get(p.suffix)
if fmt:
subprocess.run([*fmt, str(p)], capture_output=True)
sys.exit(0)
Secret Scanner: Catch Leaked Keys Before They Hit Git
# .claude/hooks/scan_secrets.py
import json, re, sys
from pathlib import Path
data = json.load(sys.stdin)
file_path = data.get("tool_input", {}).get("file_path", "")
if not file_path:
sys.exit(0)
PATTERNS = {
"Anthropic": r"sk-ant-api\d{2}-[a-zA-Z0-9_-]{86}",
"AWS": r"AKIA[0-9A-Z]{16}",
"GitHub": r"ghp_[a-zA-Z0-9]{36}",
"Stripe": r"sk_(live|test)_[a-zA-Z0-9]{24}",
"OpenAI": r"sk-[a-zA-Z0-9]{48}",
}
EXCLUDES = [r"YOUR_KEY", r"REPLACE_ME", r"example", r"xxxx", r"test_"]
try:
content = Path(file_path).read_text(errors="replace")
except Exception:
sys.exit(0)
for name, pattern in PATTERNS.items():
for match in re.findall(pattern, content):
if not any(re.search(ex, match, re.I) for ex in EXCLUDES):
print(f"[SECRET WARNING] {name} key detected: {match[:20]}...", file=sys.stderr)
# To block instead of warn: sys.exit(2)
sys.exit(0)
Test Runner: Auto-Test on Source Changes
# .claude/hooks/test_runner.py
import json, subprocess, sys
from pathlib import Path
data = json.load(sys.stdin)
file_path = data.get("tool_input", {}).get("file_path", "")
if not file_path:
sys.exit(0)
p = Path(file_path)
if "src" not in p.parts or p.suffix != ".py" or p.name.startswith("test_"):
sys.exit(0)
test_file = Path("tests") / f"test_{p.name}"
if not test_file.exists():
sys.exit(0)
result = subprocess.run(
["pytest", str(test_file), "-q", "--no-header", "--tb=short"],
capture_output=True, text=True, timeout=60
)
print(result.stdout)
if result.returncode != 0:
print(result.stderr, file=sys.stderr)
sys.exit(0)
Available Environment Variables
| Variable | Description |
|---|---|
CLAUDE_PROJECT_DIR |
Project root directory |
CLAUDE_TOOL_NAME |
Tool being executed |
CLAUDE_TOOL_INPUT_FILE_PATH |
File path (Write/Edit tools) |
CLAUDE_TOOL_INPUT_COMMAND |
Shell command (Bash tool) |
Three Rules for Reliable Hooks
-
Always set
timeout— An infinite-looping hook will freeze Claude Code -
Catch exceptions — Hook bugs should never block the workflow (
sys.exit(0)in except blocks) - Keep hooks fast — Formatters (< 1s) are fine; full builds are not
Summary
The Hooks system turns Claude Code from a "code generator" into a "quality-enforced development environment." Set them up once, and every AI interaction automatically gets formatted, scanned for secrets, and tested.
If you want pre-built skills with built-in quality guardrails, the Security Pack (¥1,480) and Code Review Pack (¥980) are available on PromptWorks.
Myouga (@myougatheaxo) — Security-focused Claude Code engineer.
Top comments (0)