Blog / · 5 min read
Claude Code Hooks: Keep Your Codebase Clean Automatically
PreToolUse and PostToolUse hooks in Claude Code let you automate quality rules: no console.log, sorted imports, mandatory tests. Concrete configuration with Symfony and React examples.
You know Git pre-commit hooks. You might know CI hooks. But Claude Code has its own hook layer that runs in real-time while you code — and it changes everything.
Instead of discovering you left a console.log in production during review, the hook alerts you the moment you write the line. Before the file is even saved.
Two types of hooks, two key moments
Claude Code exposes two types of hooks in settings.json:
| Hook | When | Usage |
|---|---|---|
PreToolUse | Before tool execution | Block or modify an action |
PostToolUse | After tool execution | React to a result |
The tools concerned: Read, Write, Edit, Bash, and all other native Claude Code tools.
The structure in settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo 'Bash tool used'"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "/home/user/scripts/check-file.sh"
}
]
}
]
}
}
The matchers
The matcher filters which tool triggers the hook:
"Bash"→ only bash commands"Write"→ only file writes"Edit"→ only file modifications"*"→ all tools
You can also match more precise patterns:
{
"matcher": "Bash",
"hooks": [...]
}
Use case 1: Block console.log in production
The most useful hook I’ve configured: prevent Claude from adding console.log to code.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "grep -n 'console\\.log' $FILE_PATH && exit 2 || exit 0"
}
]
}
]
}
}
How it works:
- Claude attempts to write a file
- The hook executes with the
$FILE_PATHvariable - If
console.logis detected →exit 2blocks the action - Claude receives feedback and must correct
Symfony version: block dd() and dump()
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "grep -E '(dd\\(|dump\\()' $FILE_PATH && echo 'DEBUG FUNCTIONS FORBIDDEN' && exit 2 || exit 0"
}
]
}
]
}
}
Use case 2: Force tests before commit
You want Claude to verify tests pass before considering a task complete.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "if echo \"$COMMAND\" | grep -q 'git commit'; then bun test; fi"
}
]
}
]
}
}
Note: This hook runs tests after every commit. If tests fail, you see it immediately.
Symfony version with PHPUnit
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "if echo \"$COMMAND\" | grep -q 'git commit'; then php bin/phpunit --testdox; fi"
}
]
}
]
}
}
Use case 3: Automatically sorted imports
For TypeScript/React projects, force import sorting:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "if echo \"$FILE_PATH\" | grep -qE '\\.(ts|tsx)$'; then npx sort-imports $FILE_PATH; fi"
}
]
}
]
}
}
Use case 4: TypeScript type checking
Block writes if TypeScript isn’t satisfied:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "if echo \"$FILE_PATH\" | grep -qE '\\.(ts|tsx)$'; then npx tsc --noEmit; fi"
}
]
}
]
}
}
Variables available in hooks
| Variable | Description |
|---|---|
$FILE_PATH | File path (for Write/Edit/Read) |
$COMMAND | Bash command executed (for Bash) |
$TOOL_INPUT | Complete tool input as JSON |
Exit codes and behavior
| Code | Effect |
|---|---|
0 | Success, action continues |
2 | Blocking, action is cancelled |
| Other | Non-blocking, warning displayed |
Complete configuration for a full-stack project
Here’s my settings.json configuration for a Symfony + React project:
{
"env": {
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
},
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/pre-write.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/post-bash.sh"
}
]
}
]
}
}
pre-write.sh
#!/bin/bash
FILE_PATH="$1"
CONTENT=$(cat)
# Block console.log in JS/TS
if echo "$FILE_PATH" | grep -qE '\.(js|jsx|ts|tsx)$'; then
if echo "$CONTENT" | grep -q 'console\.log'; then
echo "BLOCKED: console.log detected in $FILE_PATH"
exit 2
fi
fi
# Block dd()/dump() in PHP
if echo "$FILE_PATH" | grep -qE '\.php$'; then
if echo "$CONTENT" | grep -qE '(dd\(|dump\()'; then
echo "BLOCKED: dd() or dump() detected in $FILE_PATH"
exit 2
fi
fi
exit 0
post-bash.sh
#!/bin/bash
COMMAND="$1"
# After a commit, run tests
if echo "$COMMAND" | grep -q 'git commit'; then
echo "Running tests after commit..."
# PHP tests
if [ -f "composer.json" ]; then
php bin/phpunit --testdox --colors=always
fi
# JS tests
if [ -f "package.json" ]; then
bun test
fi
fi
exit 0
Limitations to know
- Performance: Hooks slow down every operation. Keep them lightweight.
- Infinite loops: A hook that modifies a file can trigger another hook. Watch for side effects.
- Debug: Hook errors appear in Claude Code logs, not always directly visible.
What this changes daily
With Claude Code hooks, you move from a “code → commit → CI → discover bug” workflow to “code → immediate feedback → commit clean code”.
It’s the difference between fixing a problem 30 seconds after creating it vs discovering it in review 2 hours later. Multiplied by the number of times Claude Code works on your codebase.
The most impactful hooks in my practice:
- Block debug statements (
console.log,dd(),dump()) - Run tests after commit (immediate feedback)
- Check TypeScript types (catch errors before they reach runtime)
Minimal configuration to start: block console.log. You’ll thank yourself later.
Keep reading