Kevin Aubrée

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.

Claude Code Hooks: Keep Your Codebase Clean Automatically

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:

HookWhenUsage
PreToolUseBefore tool executionBlock or modify an action
PostToolUseAfter tool executionReact 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:

  1. Claude attempts to write a file
  2. The hook executes with the $FILE_PATH variable
  3. If console.log is detected → exit 2 blocks the action
  4. 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

VariableDescription
$FILE_PATHFile path (for Write/Edit/Read)
$COMMANDBash command executed (for Bash)
$TOOL_INPUTComplete tool input as JSON

Exit codes and behavior

CodeEffect
0Success, action continues
2Blocking, action is cancelled
OtherNon-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:

  1. Block debug statements (console.log, dd(), dump())
  2. Run tests after commit (immediate feedback)
  3. Check TypeScript types (catch errors before they reach runtime)

Minimal configuration to start: block console.log. You’ll thank yourself later.

Kevin Aubrée

Keep reading

Back to blog