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 theWritetool - 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:
- Claude prepares tool parameters and generates the JSON payload
PreToolUsehooks execute sequentially- If any hook returns exit code 2 or
"permissionDecision":"deny", the tool is blocked immediately - The tool executes (if allowed)
PostToolUsehooks 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:
~/.claude/settings.json(global user settings).claude/settings.json(project-local settings)- 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_inputfields; sanitize before processing - Use explicit environment variables: When configuring
httptype hooks, explicitly declareallowedEnvVarsto prevent secret leakage - Blocking semantics: Return exit code 2 or
"permissionDecision":"deny"inPreToolUsehooks to prevent dangerous operations from executing - Timeout constraints: Always set reasonable
timeoutvalues (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_namefield 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.jsonor.claude/settings.jsonusing the nested hooks schema documented in06-hooks/README.md - The
luongnv89/claude-howtorepository provides production-ready examples includingvalidate-bash.pyfor security,format-code.shfor automation, andauto-adapt-mode.pyfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →