Configuring Claude Code Hooks for Specific Tools: A Complete Implementation Guide

Claude Code hooks enable automatic execution of custom scripts when specific tools like Write, Bash, or Read are invoked, allowing you to enforce security policies, format code, or validate commands before they run.

The luongnv89/claude-howto repository provides a comprehensive reference implementation for Claude Code's extensible hook architecture. By configuring matchers that target specific tool names in ~/.claude/settings.json, you can intercept operations at PreToolUse or PostToolUse events to inject custom validation, automation, or logging logic directly into Claude Code's execution flow.

Understanding the Hook Architecture

Claude Code exposes 25 distinct hook events, including PreToolUse, PostToolUse, and UserPromptSubmit. Each event communicates via JSON payloads delivered through stdin, expecting a JSON response on stdout. According to the source documentation in 06-hooks/README.md, the system supports four hook types: command (shell scripts), http (web endpoints), prompt (interactive confirmation), and agent (sub-agent invocation).

The configuration schema follows a nested structure where events contain matchers, and matchers contain hook definitions:

{
  "hooks": {
    "<EventName>": [
      {
        "matcher": "<ToolPattern>",
        "hooks": [
          {
            "type": "command",
            "command": "<script-path>",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Hooks execute with the same permissions as the Claude Code process, receiving a payload containing session_id, tool_name, tool_input, and transcript_path. This payload allows hooks to return permissionDecision values of "allow", "deny", or "ask", and even modify tool inputs via the updatedInput field before execution.

How Tool Matching Works

The matcher field determines which tools trigger your hook. The evaluation supports three patterns against the incoming JSON's "tool_name" field:

  • Exact match: "Write" fires only for the Write tool
  • Regular expressions: "Write|Edit" fires for either tool
  • Wildcard: "*" or an empty string matches all tools

Matching is case-sensitive, so "write" does not match "Write" unless you implement a case-insensitive regex pattern. This specificity allows you to bind distinct validation logic to individual tools without affecting Claude Code's broader operation.

Execution Flow: PreToolUse vs PostToolUse

The hook system implements a strict execution pipeline:

  1. Claude prepares tool parameters and generates the JSON payload
  2. PreToolUse hooks execute sequentially
  3. If any hook returns exit code 2 or "permissionDecision":"deny", the tool is blocked immediately
  4. The tool executes (if allowed)
  5. PostToolUse hooks run (these cannot block execution but can trigger side effects)

This architecture makes PreToolUse ideal for security validation and PostToolUse suitable for formatting, logging, or cleanup operations.

Practical Hook Implementations by Tool

Blocking Dangerous Bash Commands

For the Bash tool, implement a PreToolUse hook that validates commands against dangerous patterns. The reference implementation in 06-hooks/validate-bash.py demonstrates blocking rm -rf / and sudo rm operations:

#!/usr/bin/env python3
import json, sys, re

BLOCKED = [
    (r"\brm\s+-rf\s+/", "Blocking rm -rf"),
    (r"\bsudo\s+rm", "Blocking sudo rm")
]

payload = json.load(sys.stdin)
if payload.get("tool_name") != "Bash":
    sys.exit(0)

cmd = payload.get("tool_input", {}).get("command", "")
for pat, msg in BLOCKED:
    if re.search(pat, cmd):
        print(msg, file=sys.stderr)
        sys.exit(2)   # Exit code 2 blocks execution

sys.exit(0)

Configure this in ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Auto-Formatting Code After Writes

Use PostToolUse hooks to automatically format files after Write or Edit operations. The 06-hooks/format-code.sh script detects file extensions and applies appropriate formatters:

#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | python3 -c "import sys, json; print(json.load(sys.stdin).get('tool_name',''))")
FILE=$(echo "$INPUT" | python3 -c "import sys, json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))")

if [[ "$TOOL" != "Write" && "$TOOL" != "Edit" ]]; then
  exit 0
fi

case "$FILE" in
  *.js|*.jsx|*.ts|*.tsx|*.json) command -v prettier &>/dev/null && prettier --write "$FILE" ;;
  *.py)               command -v black &>/dev/null && black "$FILE" ;;
  *.go)               command -v gofmt &>/dev/null && gofmt -w "$FILE" ;;
esac
exit 0

The corresponding configuration uses a regex matcher:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/format-code.sh\"",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Scanning for Hard-Coded Secrets

Implement security scanning for the Write tool using 06-hooks/security-scan.sh to detect potential secret leakage:

#!/bin/bash
FILE=$1
if [[ -z "$FILE" ]]; then exit 0; fi

echo "🔒 Scanning $FILE for secrets..."
if grep -qE "(password|api[_-]?key|secret).*=" "$FILE"; then
  echo "⚠️  Potential secret detected in $FILE"
fi
exit 0

Auto-Adapting Permissions Based on Usage

The advanced 06-hooks/auto-adapt-mode.py script demonstrates learning safe permission patterns from successful tool executions. It runs on PostToolUse with a wildcard matcher to automatically expand ~/.claude/settings.json permissions for frequently used Bash commands:

#!/usr/bin/env python3
import json, sys, os, re

SETTINGS = os.path.expanduser("~/.claude/settings.json")
BASELINE = ["Read(*)","Write(*)","Bash(ls:*)","Bash(cat:*)"]

def load():
    return json.load(open(SETTINGS)) if os.path.exists(SETTINGS) else {}

def save(cfg):
    with open(SETTINGS,"w") as f:
        json.dump(cfg,f,indent=2)
        f.write("\n")

payload = json.load(sys.stdin)
tool = payload.get("tool_name")
inp = payload.get("tool_input", {})

if tool == "Bash":
    cmd = inp.get("command","")
    parts = cmd.split()
    if parts:
        rule = f"Bash({parts[0]}:*)"
        cfg = load()
        allow = cfg.setdefault("permissions",{}).setdefault("allow",[])
        if rule not in allow and not any(re.search(p, cmd) for p in ["rm -rf","force"]):
            allow.append(rule)
            save(cfg)

sys.exit(0)

Configuration for global monitoring:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/auto-adapt-mode.py\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Configuration File Locations

Claude Code searches for hook configurations in three locations, in order of precedence:

  1. ~/.claude/settings.json (global user settings)
  2. .claude/settings.json (project-local settings)
  3. Plugin directories containing hooks/hooks.json

The CLAUDE_PROJECT_DIR environment variable is automatically set by Claude Code, allowing scripts to reference project-relative paths reliably across different environments.

Security Considerations and Best Practices

When configuring Claude Code hooks for specific tools, adhere to these security principles from the 06-hooks/README.md documentation:

  • Validate all inputs: Never trust user-provided data in tool_input fields; sanitize before processing
  • Use explicit environment variables: When configuring http type hooks, explicitly declare allowedEnvVars to prevent secret leakage
  • Blocking semantics: Return exit code 2 or "permissionDecision":"deny" in PreToolUse hooks to prevent dangerous operations from executing
  • Timeout constraints: Always set reasonable timeout values (in seconds) to prevent hooks from hanging indefinitely
  • Permission parity: Remember hooks execute with Claude Code's permissions, so avoid elevated privileges in hook scripts

Summary

  • Tool-specific hooks use matchers targeting the tool_name field in JSON payloads delivered via stdin
  • PreToolUse hooks can block operations by returning exit code 2 or "permissionDecision":"deny", while PostToolUse hooks cannot block but can trigger side effects
  • Configuration resides in ~/.claude/settings.json or .claude/settings.json using the nested hooks schema documented in 06-hooks/README.md
  • The luongnv89/claude-howto repository provides production-ready examples including validate-bash.py for security, format-code.sh for automation, and auto-adapt-mode.py for dynamic permission management
  • Hooks must respond with valid JSON on stdout and respect the timeout parameter to ensure smooth Claude Code operation

Frequently Asked Questions

How do I target multiple specific tools with one hook configuration?

Use a regular expression in the matcher field. For example, "Write|Edit" applies the hook to both the Write and Edit tools, while "*" matches all tools. The matching is case-sensitive and evaluates against the tool_name field in the incoming JSON payload.

Can I modify tool input parameters before Claude Code executes them?

Yes. In a PreToolUse hook, return a JSON object containing an updatedInput field with the modified parameters. Claude Code will use these updated values instead of the original inputs. This allows you to sanitize paths, add flags, or transform arguments before execution.

What is the difference between PreToolUse and PostToolUse hooks?

PreToolUse hooks execute before the tool runs and can block execution by returning exit code 2 or "permissionDecision":"deny". PostToolUse hooks execute after the tool completes and cannot block, but they can perform actions like logging, formatting, or updating configuration files based on the operation's results.

Where should I store hook scripts for team-wide consistency?

Store hook scripts in your project's .claude/hooks/ directory and reference them using the $CLAUDE_PROJECT_DIR environment variable in your configuration. This ensures the scripts are version-controlled and accessible to all team members, while individual developers can still maintain personal hooks in ~/.claude/settings.json.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →