Skills Development: SKILL.md Complete Parameters, Dynamic Context Injection and Invocation Control Matrix
Chapter 42: Claude Code Hooks: pre-tool, post-tool, and Automated Quality Gating
42.1 The Motivation Behind Hooks
Claude Code is a powerful AI programming assistant, but when used in team and production environments, you need a mechanism to ensure that every operation Claude performs conforms to your standards — not just by telling Claude what the standards are through CLAUDE.md, but by automatically enforcing those standards before and after actions occur.
That is what Claude Code Hooks are for.
Hooks are shell command triggers that fire before and after Claude executes tool calls. They are configured in .claude/settings.json and run completely independently of Claude's AI decision-making. Regardless of what Claude decides to do, the hooks execute your specified commands at the defined points.
Core value: Hooks transform team conventions from "suggestions" into "enforced rules."
Consider this scenario: your team requires that all Python files be formatted with Black after modification. With a post-tool hook, every time Claude writes or modifies a .py file, Black runs automatically — no need to remind Claude, no reliance on Claude's initiative. The formatting just happens.
42.2 The Hook Configuration Format
Hooks are configured in .claude/settings.json, the project-level Claude Code configuration file, which should be committed to version control:
{
"hooks": {
"pre-tool": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "echo 'About to modify files...' >> .claude/hooks.log"
}
]
}
],
"post-tool": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "scripts/post-write-hook.sh"
}
]
}
]
}
}
Configuration Structure
hooks
├── pre-tool # List of hooks that fire before tool execution
│ └── [Hook Entry]
│ ├── matcher # Which tools to match (regex)
│ └── hooks # List of commands to run
│ └── [Hook]
│ ├── type # Currently only "command" is supported
│ └── command # The shell command to execute
└── post-tool # List of hooks that fire after tool execution
└── [same structure]
Writing Matchers
matcher is a regex string that matches against tool names. The primary Claude Code tool names are:
File operations:
- Write # Write a new file
- Edit # Edit an existing file (single replacement)
- MultiEdit # Multiple edits in one call
- Read # Read a file
Shell operations:
- Bash # Execute a shell command
Search tools:
- Grep # Text content search
- Glob # Filename pattern search
Others:
- WebFetch # Fetch web page content
- TodoWrite # Write to the Todo list
Matcher examples:
"matcher": "Write|Edit|MultiEdit" // match all write operations
"matcher": "Bash" // match only the Bash tool
"matcher": ".*" // match all tools
"matcher": "Write" // match only Write
42.3 pre-tool Hooks: Execute Before an Action
pre-tool hooks fire before Claude executes a tool. Their primary uses are:
- Pre-checks: verify that preconditions are met
- Permission checks: validate authorization for sensitive operations
- Notifications: send alerts before important operations
- Audit logging: record operations that are about to occur
Example 1: Prevent Direct Commits to the Main Branch
{
"hooks": {
"pre-tool": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "scripts/check-branch.sh"
}
]
}
]
}
}
scripts/check-branch.sh:
#!/bin/bash
# Check current git branch; warn if on main or master
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
echo "WARNING: You are on the $BRANCH branch."
echo "Direct commits to $BRANCH are discouraged."
echo "Consider: git checkout -b feature/your-feature"
# A non-zero exit code would block tool execution.
# Here we only warn, so we exit 0:
exit 0
fi
exit 0
Example 2: Audit Log for All File Operations
{
"hooks": {
"pre-tool": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "echo \"$(date -Iseconds) PRE-TOOL: $CLAUDE_TOOL_NAME $CLAUDE_TOOL_INPUT\" >> .claude/audit.log"
}
]
}
]
}
}
The Meaning of pre-tool Exit Codes
The exit code of a pre-tool hook has critical significance:
- Exit code 0: hook succeeded; allow the tool to proceed
- Non-zero exit code: hook failed; Claude Code blocks the tool from executing and reports the error to the user
This makes pre-tool hooks genuine "quality gates" — you can write check scripts that directly prevent Claude's actions when conditions are not met.
Example 3: Block Modifications to Production Config Files
#!/bin/bash
# pre-write-check.sh
# Prevent Claude from modifying specific production config files
PROTECTED_FILES=(
"config/production.yaml"
"config/production.json"
".env.production"
"kubernetes/prod/"
)
TARGET_FILE="${CLAUDE_TOOL_INPUT_FILE_PATH:-}"
for protected in "${PROTECTED_FILES[@]}"; do
if [[ "$TARGET_FILE" == *"$protected"* ]]; then
echo "ERROR: Modification of protected production config is not allowed."
echo "File: $TARGET_FILE"
echo "Use the deployment pipeline to change production configs."
exit 1 # non-zero exit code blocks the operation
fi
done
exit 0
42.4 post-tool Hooks: Execute After an Action
post-tool hooks fire after Claude's tool execution completes. Their primary uses are:
- Auto-formatting: format code immediately after it is written
- Automated testing: run related tests immediately after code changes
- Validation: verify that modifications conform to conventions
- Build triggering: trigger incremental builds after code changes
- Notifications: send alerts after operations complete
Example 1: Automatic Python Formatting
{
"hooks": {
"post-tool": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "scripts/post-write-format.sh"
}
]
}
]
}
}
scripts/post-write-format.sh:
#!/bin/bash
# Auto-format and lint Python files after modification
MODIFIED_FILE="${CLAUDE_TOOL_INPUT_FILE_PATH:-}"
if [[ "$MODIFIED_FILE" == *.py ]]; then
echo "Running Black formatter on $MODIFIED_FILE..."
black "$MODIFIED_FILE" 2>&1
echo "Running isort on $MODIFIED_FILE..."
isort "$MODIFIED_FILE" 2>&1
echo "Running flake8 lint check..."
flake8 "$MODIFIED_FILE" 2>&1
LINT_EXIT=$?
if [ $LINT_EXIT -ne 0 ]; then
echo "WARNING: flake8 found issues in $MODIFIED_FILE"
fi
fi
exit 0
Example 2: TypeScript Type Checking
{
"hooks": {
"post-tool": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "scripts/ts-typecheck.sh"
}
]
}
]
}
}
scripts/ts-typecheck.sh:
#!/bin/bash
# Type-check modified TypeScript files
MODIFIED_FILE="${CLAUDE_TOOL_INPUT_FILE_PATH:-}"
if [[ "$MODIFIED_FILE" == *.ts ]] || [[ "$MODIFIED_FILE" == *.tsx ]]; then
echo "Running TypeScript type check..."
npx tsc --noEmit --project tsconfig.json 2>&1
TS_EXIT=$?
if [ $TS_EXIT -ne 0 ]; then
echo "TypeScript type errors found. Claude should review and fix."
exit 1 # signals to Claude that there is a problem to address
fi
fi
exit 0
When a post-tool hook returns a non-zero exit code, Claude Code feeds the error output back to Claude, which can then continue working to fix the problem. This creates an automatic feedback loop: Claude modifies code → hook runs type check → errors found → Claude is notified → Claude keeps fixing.
42.5 Environment Variables: How Hooks Access Context
Hooks receive context about the current operation through environment variables:
# Environment variables injected into hook processes by Claude Code
CLAUDE_TOOL_NAME # Name of the tool that triggered the hook (e.g., "Write")
CLAUDE_TOOL_INPUT # Full tool input as JSON
CLAUDE_TOOL_INPUT_FILE_PATH # For file operations, the target file path
CLAUDE_TOOL_INPUT_COMMAND # For Bash tool, the command being run
CLAUDE_TOOL_OUTPUT # (post-tool only) The tool's output
CLAUDE_SESSION_ID # Current session ID
CLAUDE_PROJECT_DIR # Project root directory path
Parsing JSON Input
For hooks that need more detailed information, parse CLAUDE_TOOL_INPUT:
#!/bin/bash
# Parse tool input for detailed information
TOOL_INPUT="${CLAUDE_TOOL_INPUT:-{}}"
# Use jq to parse JSON
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty')
CONTENT=$(echo "$TOOL_INPUT" | jq -r '.content // empty')
COMMAND=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
echo "Tool: $CLAUDE_TOOL_NAME"
echo "File: $FILE_PATH"
echo "Command: $COMMAND"
42.6 Building a Complete Automated Quality Gate
By combining pre-tool and post-tool hooks, you can build a comprehensive automated quality gate. Here is a complete example for a Node.js project:
{
"hooks": {
"pre-tool": [
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "scripts/hooks/pre-bash.sh"}]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [{"type": "command", "command": "scripts/hooks/pre-write.sh"}]
}
],
"post-tool": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [{"type": "command", "command": "scripts/hooks/post-write.sh"}]
},
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "scripts/hooks/post-bash.sh"}]
}
]
}
}
scripts/hooks/post-write.sh (the core quality gate):
#!/bin/bash
MODIFIED_FILE="${CLAUDE_TOOL_INPUT_FILE_PATH:-}"
EXIT_CODE=0
# 1. JavaScript/TypeScript files
if [[ "$MODIFIED_FILE" =~ \.(js|jsx|ts|tsx)$ ]]; then
# Auto-format with Prettier (modifies the file in place)
if command -v prettier &>/dev/null; then
prettier --write "$MODIFIED_FILE" 2>&1
echo "✓ Prettier formatting applied"
fi
# ESLint check (report, do not auto-fix)
if command -v eslint &>/dev/null; then
eslint "$MODIFIED_FILE" 2>&1
ESLINT_EXIT=$?
if [ $ESLINT_EXIT -ne 0 ]; then
echo "✗ ESLint found issues"
EXIT_CODE=$ESLINT_EXIT
else
echo "✓ ESLint passed"
fi
fi
fi
# 2. Test file detection
if [[ "$MODIFIED_FILE" =~ src/(.+)\.(ts|js)$ ]] && [[ ! "$MODIFIED_FILE" =~ \.(test|spec)\. ]]; then
RELATIVE_PATH="${BASH_REMATCH[1]}"
TEST_FILE="src/${RELATIVE_PATH}.test.ts"
SPEC_FILE="src/${RELATIVE_PATH}.spec.ts"
if [ ! -f "$TEST_FILE" ] && [ ! -f "$SPEC_FILE" ]; then
echo "WARNING: No test file found for $MODIFIED_FILE"
echo "Consider creating: $TEST_FILE"
fi
fi
# 3. Detect package.json changes
if [[ "$MODIFIED_FILE" == "package.json" ]]; then
echo "package.json modified. Running npm install to sync node_modules..."
npm install 2>&1
fi
exit $EXIT_CODE
42.7 Hooks and CLAUDE.md: Complementary Tools
Hooks and CLAUDE.md are complementary tools that address different layers of the problem:
| Dimension | CLAUDE.md | Hooks |
|---|---|---|
| Execution | Influences Claude's AI decisions | Independent of AI; shell commands |
| Reliability | Depends on Claude following rules | Mechanically enforced |
| Best for | Style guides, architecture decisions | Formatting, linting, testing |
| Bypassable | Claude may forget or ignore | Cannot be ignored |
| Flexibility | High (natural language) | Low (must be executable scripts) |
The best practice is to use both together:
- CLAUDE.md communicates norms and intent to the AI
- Hooks mechanically enforce quality checks that can be automated
For example, CLAUDE.md states "all TypeScript files must pass type checking," and a post-tool hook is configured to run tsc --noEmit automatically after every file write. Even if Claude forgets the convention, the hook will catch the problem and feed it back.
42.8 Debugging Hooks
When hooks go wrong, debugging can be challenging. Here are some practical techniques.
Enable Hook Logging
# Add logging at the start of each hook script
LOG_FILE=".claude/hooks-debug.log"
echo "$(date -Iseconds) HOOK: $CLAUDE_TOOL_NAME" >> "$LOG_FILE"
echo " INPUT: $CLAUDE_TOOL_INPUT" >> "$LOG_FILE"
Document Debugging in CLAUDE.md
## Hook Debugging
If hooks are behaving unexpectedly, check .claude/hooks-debug.log.
Run `scripts/hooks/test-hooks.sh` to test all hooks without involving Claude.
Test Hook Scripts Locally
# Simulate Claude Code's environment variables and test a hook directly
CLAUDE_TOOL_NAME="Write" \
CLAUDE_TOOL_INPUT_FILE_PATH="src/utils/format.ts" \
CLAUDE_TOOL_INPUT='{"file_path":"src/utils/format.ts","content":"export function foo() {}"}' \
bash scripts/hooks/post-write.sh
Summary
Claude Code Hooks are the key mechanism for elevating AI-assisted programming from "inconsistent quality" to "predictable quality."
Key takeaways:
- Hooks are configured in
.claude/settings.jsonas shell commands that run independently of AI decision-making - pre-tool hooks can check conditions before an action; a non-zero exit code blocks the operation
- post-tool hooks can automatically format code, run type checks, and execute tests after writes
- Context about the current operation is available through environment variables such as
CLAUDE_TOOL_NAMEandCLAUDE_TOOL_INPUT_FILE_PATH - Hooks and CLAUDE.md are complementary: CLAUDE.md works at the AI decision layer; hooks work at the mechanical enforcement layer
- Combining pre-tool and post-tool hooks enables a full automated quality gate system