TF
Tech Frontier

Claude Code Hooks: Auto-Format, Security Guards, and Test Triggers on Every Tool Call

Mar 11, 2026 384 views

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

  1. Always set timeout — An infinite-looping hook will freeze Claude Code
  2. Catch exceptions — Hook bugs should never block the workflow (sys.exit(0) in except blocks)
  3. 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.

👉 prompt-works.jp

Myouga (@myougatheaxo) — Security-focused Claude Code engineer.

Related Articles