AI Coding Safety Guide: Adding Brakes and Seat Belts to Claude Code with Hooks
4/17/2026
查看这篇文章的中文版本Introduction
“Claude Code is indeed fast at writing code, but what if it executes
rm -rf /?”
This is the most genuine anxiety many developers have when they first start using Claude Code.
AI programming tools are getting increasingly powerful — automatically reading files, writing code, running commands — efficiency is truly taking off. But the problems follow: you can’t control every single action it takes. You want automatic formatting when it modifies code, security checks when it runs Bash commands, and secret leak scanning when it writes files…
If you have frontend experience, you’re surely familiar with Vue lifecycle hooks like mounted and beforeDestroy. If you’ve worked on the backend, Spring’s @Before and @After AOP aspects are no strangers. Their shared essence: injecting custom logic at specific event points. That’s exactly what Claude Code’s Hook mechanism does.
In this article, based on Anthropic’s official documentation and the popular GitHub tutorial claude-howto, I’ll use familiar frontend and backend concepts as analogies to help you thoroughly understand Claude Code’s Hook mechanism, and provide 6 practical configurations you can copy and use right away.
1. What is a Hook? One Sentence to Explain It All
Hook = automatically executing your pre-set logic when a specific event occurs.
Whether it’s Vue, Spring, or Claude Code, the core idea is identical:
| Framework | Hook Mechanism | Essence |
|---|---|---|
| Vue | mounted(), updated() | Component lifecycle callback functions |
| React | useEffect(() => {}, []) | Side effect hooks for function components |
| Spring | @Before, @AfterReturning | AOP aspects, intercepting before and after method calls |
| Claude Code | PreToolUse, PostToolUse | Event hooks for the tool invocation lifecycle |
Here’s a diagram to understand Claude Code’s Hook workflow:

See? This is very similar to Spring AOP’s @Before → method execution → @After pattern.
2. Four Types of Hooks
Claude Code provides four Hook types to cover different scenarios:
| Type | Description | Analogy |
|---|---|---|
| command | Execute a Shell script, receive JSON via stdin, return decisions via exit code | Most versatile, like a Shell script |
| http | POST JSON to a remote URL, suitable for integrating with internal systems | Like a Webhook, notifying your service |
| prompt | Have the LLM evaluate the current state and return a decision | Let AI judge whether to proceed |
| agent | Launch a dedicated sub-agent that can use tools for multi-step reasoning | An enhanced version of prompt — can actually take action |
Key distinction: prompt can only “think,” while agent can “think” and “do.” For example, if you want to verify that code passes tests, prompt can only judge from the text, while agent can actually run the tests.
Context retrieval methods: Different types retrieve event context differently:
| Type | Context Source | Example |
|---|---|---|
command | Receive JSON via stdin (read with cat) | input=$(cat) |
http | JSON carried in the POST request body | Server parses from request body |
prompt | Template variable $ARGUMENTS auto-injected | "prompt": "Check if task is complete. $ARGUMENTS" |
agent | Template variable $ARGUMENTS auto-injected | "prompt": "Run the tests. $ARGUMENTS" |
Here, $ARGUMENTS is a built-in template variable in Claude Code. At runtime, it gets replaced with the current Hook event’s JSON input data (containing fields like session ID, tool name, tool parameters, etc. — the exact contents vary by event type). If the prompt string doesn’t include $ARGUMENTS, the JSON input is automatically appended to the end of the prompt. This way, the LLM can make judgments based on real event context, not just your static prompt.
3. The Complete Map of 26 Lifecycle Events
Claude Code currently supports 26 Hook events, organized into 7 groups by phase. Let’s start with the full picture to get an overall impression:

The diagram above shows the complete lifecycle of a Claude Code session from top to bottom. Let me quickly go through each group’s responsibilities:
🔄 Session Lifecycle (6 events): Manages the session’s own state — startup, shutdown, loading configuration, directory switching, file change monitoring. Most are non-interceptable and serve as “notification” events.
🔧 Tool Invocation Lifecycle (5 events): This is the most critical group. PreToolUse intercepts before tool execution (analogous to @Before), PostToolUse appends context after execution (analogous to @AfterReturning), and PostToolUseFailure handles exceptions (analogous to @AfterThrowing). 80% of daily Hooks are attached here.
🤖 Sub-agents & Team (3 events): Triggered when Claude Code launches sub-agents or engages in team collaboration. SubagentStop can intercept sub-agent output, and TeammateIdle can assign new tasks when teammates are idle.
📝 User Interaction (2 events): UserPromptSubmit is the “entry checkpoint” when a user submits a prompt, where you can intercept dangerous operation intents (like “drop the database and run”).
🛑 Stop & Wrap-up (4 events): Stop is the “exit checkpoint” when Claude Code finishes its response. You can use the prompt type to have AI self-review whether the task is truly complete.
🌲 Worktree & Compression (4 events): Manages Git Worktree and context compression. PreCompact can inject critical information that needs to be retained before compression.
🔗 MCP Interaction (2 events): Triggered when an MCP server requests user input, suitable for input validation or auto-filling.
> Memory tip: For security interception and automation, mastering PreToolUse, PostToolUse, UserPromptSubmit, and Stop covers most scenarios. But other events have practical value too — for example, SessionStart is commonly used to inject session history context, Notification for desktop notification reminders, and SubagentStop for reviewing sub-agent output. Just pick what you need.
4. Six Practical Scenarios — Copy and Use Directly
🛡️ Scenario 1: PreToolUse to Intercept Dangerous Bash Commands
Analogy: Spring’s @Before permission check aspect — verifying permissions before method execution.
Before Claude Code executes a Bash command, check whether the command is dangerous.
Configure settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/pre-tool-check.sh\""
}
]
}
]
}
}
The corresponding interception script:
#!/bin/bash
# File: .claude/hooks/pre-tool-check.sh
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command')
# 🚫 Directly blocked dangerous commands (using grep -F for literal matching)
blocked_patterns=(
"rm -rf /"
"rm -rf /*"
":(){ :|:& };:"
"mkfs."
"dd if=/dev/zero"
"dd if=/dev/random"
)
for pattern in "${blocked_patterns[@]}"; do
if echo "$command" | grep -qF "$pattern"; then
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"⛔ Dangerous command detected: $command\"}}"
exit 0
fi
done
# ⚠️ Warn but allow through (using grep -E for regex matching)
warn_patterns=("rm -rf" "git push --force" "git reset --hard" "DROP TABLE" "sudo rm")
for pattern in "${warn_patterns[@]}"; do
if echo "$command" | grep -qE "$pattern"; then
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"⚠️ Warning: Sensitive operation detected: $pattern, but allowed through\"}}"
exit 0
fi
done
exit 0
✨ Scenario 2: PostToolUse Auto-format Code
Analogy: Vue’s updated() hook — automatically executing side effects after DOM updates.
Every time Claude Code modifies a file using the Write or Edit tool, automatically run the formatter:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/format-code.sh\""
}
]
}
]
}
}
The corresponding formatting script:
#!/bin/bash
# File: .claude/hooks/format-code.sh
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
ext="${file_path##*.}"
case "$ext" in
js|ts|jsx|tsx) npx prettier --write "$file_path" 2>/dev/null ;;
py) python3 -m black "$file_path" 2>/dev/null ;;
go) gofmt -w "$file_path" 2>/dev/null ;;
java) google-java-format -i "$file_path" 2>/dev/null ;;
rs) rustfmt "$file_path" 2>/dev/null ;;
esac
exit 0
Result: Code written by Claude Code always maintains consistent formatting. No more manually running formatters.
🔍 Scenario 3: PostToolUse Security Scanning
Analogy: Spring’s @AfterReturning logging aspect — recording logs after a method returns normally.
Scan files written by Claude Code to detect hardcoded secrets and passwords.
Configure settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/security-scan.sh\""
}
]
}
]
}
}
The corresponding security scanning script:
#!/bin/bash
# File: .claude/hooks/security-scan.sh
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
# Skip sensitive directories
[[ "$file_path" =~ \.env|\.git/ ]] && exit 0
warnings=""
# Check common secret patterns (note: macOS BSD grep doesn't support \x27, use single-quote variable instead)
QUOTE="'"
if grep -qE "(password|passwd|pwd)\s*=\s*[\"${QUOTE}][^\"${QUOTE}]+[\"${QUOTE}]" "$file_path" 2>/dev/null; then
warnings+="⚠️ Hardcoded password found\n"
fi
if grep -qE "(api_key|apikey|secret_key|access_token)\s*=\s*[\"${QUOTE}][^\"${QUOTE}]+[\"${QUOTE}]" "$file_path" 2>/dev/null; then
warnings+="⚠️ Hardcoded API key found\n"
fi
if grep -qE '-----BEGIN (RSA |EC )?PRIVATE KEY-----' "$file_path" 2>/dev/null; then
warnings+="🚨 Private key file found\n"
fi
if [ -n "$warnings" ]; then
# Return as additionalContext, don't block the flow
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"Security scan found the following issues:\n$warnings\"}}"
fi
exit 0
📊 Scenario 4: Stop — Using prompt Hook to Check Task Completion
Analogy: This has no direct counterpart in Vue/Spring — it’s unique to Claude Code: letting AI audit itself.
When Claude Code considers its work done, have the LLM evaluate once more:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Please evaluate whether Claude has completed all tasks requested by the user. Check the following: 1) Have all files been modified 2) Are there any missing features 3) Do the tests pass. If the task is not complete, explain what's still missing. $ARGUMENTS",
"timeout": 30
}
]
}
]
}
}
More powerful agent version: Actually run the tests to verify:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Run the project test suite and verify all tests pass. If any tests fail, report the failure reasons. $ARGUMENTS",
"timeout": 120
}
]
}
]
}
}
🔔 Scenario 5: Notification Desktop Alerts
Analogy: Vue’s watch reactive listener — triggering callbacks when data changes.
When Claude Code needs your attention (e.g., permission dialog, long wait), send a macOS desktop notification:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\" sound name \"default\"'"
}
]
}
]
}
}
For Linux users, replace with:
"command": "notify-send 'Claude Code' 'Needs your attention'"
📝 Scenario 6: UserPromptSubmit to Intercept Dangerous Prompts
Intercept dangerous operation intents at the point when the user submits a prompt.
Configure settings.json:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-prompt.sh\""
}
]
}
]
}
}
The corresponding prompt validation script:
#!/bin/bash
# File: .claude/hooks/validate-prompt.sh
input=$(cat)
prompt=$(echo "$input" | jq -r '.prompt')
blocked_keywords=("删除数据库" "drop database" "rm -rf /" "format c:" "删库跑路")
for keyword in "${blocked_keywords[@]}"; do
if echo "$prompt" | grep -qi "$keyword"; then
echo "{\"decision\":\"block\",\"reason\":\"⛔ Your prompt contains a dangerous operation: $keyword. Blocked by security hook.\"}" >&2
exit 2
fi
done
exit 0
5. Configuration Guide
Configuration File Locations
Hooks can be configured in multiple locations, with priority from highest to lowest:
| Location | Scope | Commit to Git? |
|---|---|---|
~/.claude/settings.json | All projects | No (personal settings) |
.claude/settings.json | Current project | Yes (team shared) |
.claude/settings.local.json | Current project | No (local override) |
Core Configuration Structure
{
"hooks": {
"EventName": [
{
"matcher": "Tool name pattern",
"hooks": [
{
"type": "command",
"command": "your/script/path",
"timeout": 60
}
]
}
]
}
}
Matcher Pattern Rules
> Note: UserPromptSubmit, Stop, TeammateIdle, TaskCreated, TaskCompleted, WorktreeCreate, WorktreeRemove, and CwdChanged events do not support matchers. They execute every time they are triggered, and configured matchers are silently ignored. Matchers are primarily for tool-related events (such as PreToolUse, PostToolUse, etc.).
| Pattern | Description | Example |
|---|---|---|
| Exact match | Match only the specified tool | "Write" |
| Multi-select | Pipe-separated | "Edit|Write" |
| Regex match | Parsed as regex if special characters are present | "^Notebook" |
| Wildcard | Match all | "*" or "" |
| MCP tool | Match MCP server tools | "mcp__memory__.*" |
Core Mechanism of Hook Scripts
Input: Receive JSON via stdin, containing tool name, parameters, session ID, etc.
Output: Control behavior via exit code and JSON stdout:
exit 0→ Continue (stdout is parsed as JSON; you can return decisions viahookSpecificOutput)exit 2→ Block the operation (stderr is displayed as an error message)- Other exit codes → Non-blocking error, execution continues
Environment variables:
$CLAUDE_PROJECT_DIR— Absolute path to the project root directory (available in all command hooks)$CLAUDE_ENV_FILE— File path for persisting environment variables (only available in command hooks forSessionStart,CwdChanged, andFileChangedevents)
Quick Start Steps
# 1. Create hooks directory
mkdir -p .claude/hooks
# 2. Copy the scripts you need
# (Choose from the six scenarios above)
# 3. Grant execute permissions
chmod +x .claude/hooks/*.sh
# 4. Configure settings.json
# (Add the corresponding JSON configuration)
# 5. Test the Hook
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | bash .claude/hooks/pre-tool-check.sh
echo $?
# Should output 2 (blocked)
Conclusion
The Hook mechanism is the critical bridge that transforms Claude Code from an “AI black box” into a “controllable process.”
Let’s review what we covered today:
- The essence of Hooks is event-driven callbacks — the same concept as the Vue lifecycle and Spring AOP you’re already familiar with
- Four core events cover 80% of scenarios:
PreToolUse(pre-execution interception),PostToolUse(post-execution processing),UserPromptSubmit(input validation), andStop(completion check) - Four Hook types meet different needs: command (most versatile), http (external system integration), prompt (AI self-evaluation), agent (AI self-verification)
- Six practical configurations you can copy directly into your projects
Just as Spring AOP gave enterprise applications unified transaction management and security controls, Claude Code’s Hooks give AI programming safety nets and quality gates.
You don’t need to watch over every single thing AI does — just configure your Hooks and let it operate freely within the rules you’ve set.
References:
Anthropic Official Hooks Documentation: https://code.claude.com/docs/en/hooks
Claude Code Hooks Practical Tutorial (English): https://github.com/luongnv89/claude-howto/tree/main/06-hooks
Claude Code Hooks Chinese Translation: https://github.com/lhfer/claude-howto-zh-cn/tree/main/06-hooks