Skip to main content
Back to Blog

AI Coding Safety Guide: Adding Brakes and Seat Belts to Claude Code with Hooks

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:

FrameworkHook MechanismEssence
Vuemounted(), updated()Component lifecycle callback functions
ReactuseEffect(() => {}, [])Side effect hooks for function components
Spring@Before, @AfterReturningAOP aspects, intercepting before and after method calls
Claude CodePreToolUse, PostToolUseEvent 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:

TypeDescriptionAnalogy
commandExecute a Shell script, receive JSON via stdin, return decisions via exit codeMost versatile, like a Shell script
httpPOST JSON to a remote URL, suitable for integrating with internal systemsLike a Webhook, notifying your service
promptHave the LLM evaluate the current state and return a decisionLet AI judge whether to proceed
agentLaunch a dedicated sub-agent that can use tools for multi-step reasoningAn 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:

TypeContext SourceExample
commandReceive JSON via stdin (read with cat)input=$(cat)
httpJSON carried in the POST request bodyServer parses from request body
promptTemplate variable $ARGUMENTS auto-injected"prompt": "Check if task is complete. $ARGUMENTS"
agentTemplate 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:

LocationScopeCommit to Git?
~/.claude/settings.jsonAll projectsNo (personal settings)
.claude/settings.jsonCurrent projectYes (team shared)
.claude/settings.local.jsonCurrent projectNo (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.).

PatternDescriptionExample
Exact matchMatch only the specified tool"Write"
Multi-selectPipe-separated"Edit|Write"
Regex matchParsed as regex if special characters are present"^Notebook"
WildcardMatch all"*" or ""
MCP toolMatch 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 via hookSpecificOutput)
  • 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 for SessionStart, CwdChanged, and FileChanged events)

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:

  1. The essence of Hooks is event-driven callbacks — the same concept as the Vue lifecycle and Spring AOP you’re already familiar with
  2. Four core events cover 80% of scenarios: PreToolUse (pre-execution interception), PostToolUse (post-execution processing), UserPromptSubmit (input validation), and Stop (completion check)
  3. Four Hook types meet different needs: command (most versatile), http (external system integration), prompt (AI self-evaluation), agent (AI self-verification)
  4. 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