YouTip LogoYouTip

Claude Code Hooks

Claude Code Hooks | Beginner's Tutorial

\n\n

Claude Code hooks are user-defined Shell commands that automatically execute at specific nodes in the Claude Code lifecycle.

\n\n

With hooks, you can achieve precise control over Claude Code's behavior, ensuring certain operations (e.g., code formatting, logging) definitely trigger, rather than relying on the model's autonomous decisions.

\n\n

Typical Application Scenarios

\n\n

Hooks can help implement many practical functions, including:

\n\n
    \n
  • Notifications: Automatically send desktop/email alerts when Claude Code waits for input or requires permissions
  • \n
  • Auto-formatting: Run prettier after editing .ts files, execute gofmt after modifying .go files
  • \n
  • Operation logs: Record all commands executed by Claude for compliance audits or troubleshooting
  • \n
  • Code standard validation: Automatically provide feedback if generated code violates project standards (e.g., naming rules)
  • \n
  • File permission control: Prevent Claude from modifying production configuration files or sensitive directories (e.g., .env, .git)
  • \n
\n\n

Compared to constraining Claude's behavior through prompts, hooks are application-level hard rules that enforce execution upon event triggers, offering higher stability and reliability.

\n\n

Important Security Reminder

\n\n

When running hooks, current system environment credentials (e.g., environment variables, user permissions) are directly used, posing security risks:

\n\n
    \n
  • Malicious hook code may leak sensitive data (e.g., API keys, project source code)
  • \n
  • Incorrect hook commands may cause file deletion or system anomalies
  • \n
\n\nRequired Security Practices:\n\n
    \n
  1. Review command logic and permissions line-by-line before registering hooks
  2. \n
  3. Avoid executing unknown scripts in hooks
  4. \n
  5. See official documentation Security Considerations for best practices
  6. \n
\n\n
\n\n

Hook Event Types

\n\n

Claude Code provides multiple lifecycle events to bind hook commands. Each event passes different context data and affects Claude's behavior differently.

\n\n\n \n \n \n \n \n \n \n \n \n \n \n
Event NameTrigger TimingCore Function
PreToolUseBefore tool executionIntercept tool execution (e.g., block sensitive file modifications), provide adjustment suggestions to Claude
PermissionRequestWhen permission dialog appearsAutomatically approve/reject permission requests
PostToolUseAfter tool executionPerform post-processing (e.g., code formatting, logging)
UserPromptSubmitAfter user submits prompt, before Claude processesPre-process user input (e.g., add context information)
NotificationWhen Claude sends notificationsCustomize notification methods (e.g., desktop popups, SMS alerts)
StopWhen Claude completes responsePerform cleanup tasks (e.g., delete temporary files)
SubagentStopWhen sub-agent tasks completeProcess sub-agent execution results
PreCompactBefore context compressionCustomize compression rules
SessionStartWhen starting/restoring a sessionInitialize session environment (e.g., load project configurations)
SessionEndWhen session endsSave session data, clean up environment
\n\n
\n\n

Quick Start: Implement Command Logging

\n\n

This example demonstrates configuring hooks to record all Bash commands executed by Claude.

\n\n

Prerequisites

\n\n

Install jq (for JSON data parsing):

\n\n
    \n
  • macOS: brew install jq
  • \n
  • Linux: sudo apt install jq / sudo yum install jq
  • \n
  • Windows: Download official installer, or install via WSL
  • \n
\n\n

Step 1: Open Hook Configuration

\n\n

In Claude Code interface, enter /hooks, select event PreToolUse (triggers before tool execution, suitable for command logging).

\n\n

Step 2: Add Event Matcher

\n\n

Matchers define trigger conditions. To log only Bash commands:

\n\n
    \n
  1. Select + Add new matcher…
  2. \n
  3. Enter keyword Bash (triggers only when Bash tool is called)
  4. \n
\n\n> Tip: Use * to match all tools for global hooks\n\n

Step 3: Add Hook Command

\n\n

Select + Add new hook…, enter command:

\n\n
jq -r '"(.tool_input.command) - (.tool_input.description // "No description")"' >> ~/.claude/bash-command-log.txt
\n\n

Command explanation:

\n\n
    \n
  • jq -r ...: Extracts command and description from JSON, shows "No description" if missing
  • \n
  • >>: Appends content to log file in user home directory
  • \n
\n\n

Step 4: Choose Configuration Scope

\n\n

Configuration scope determines hook scope:

\n\n
    \n
  • User settings: ~/.claude/settings.json (applies to all projects)
  • \n
  • Project settings: .claude/settings.local.json (current project only)
  • \n
\n\n

Select User settings for global logging. Press Esc to exit configuration.

\n\n

Step 5: Verify Hook Configuration

\n\n
    \n
  1. Enter /hooks to view configured hooks
  2. \n
  3. Check configuration file ~/.claude/settings.json:
  4. \n
\n\n
{ \n  "hooks": { \n    "PreToolUse": [ \n      { \n        "matcher": "Bash", \n        "hooks": [ \n          { \n            "type": "command", \n            "command": "jq -r '"(.tool_input.command) - (.tool_input.description // "No description")"' >> ~/.claude/bash-command-log.txt" \n          } \n        ] \n      } \n    ] \n  }\n}
\n\n

Step 6: Test Hook Effectiveness

\n\n
    \n
  1. Instruct Claude: Help me execute the ls command (ask to execute ls command)
  2. \n
  3. Check log file: cat ~/.claude/bash-command-log.txt
  4. \n
  5. If log shows: ls - Lists files and directories, configuration succeeded
  6. \n
\n\n
\n\n

Practical Hook Examples

\n\n

Common hook configurations you can copy directly or modify as needed.

\n\n> ? Full examples: Bash Command Validator Example\n\n

Example 1: Auto-format TypeScript Files

\n\n

Auto-format .ts files after editing/writing:

\n\n
{ \n  "hooks": { \n    "PostToolUse": [ \n      { \n        "matcher": "Edit|Write", \n        "hooks": [ \n          { \n            "type": "command", \n            "command": "jq -r '.tool_input.file_path' | { read file_path; if echo "$file_path" | grep -q '.ts$'; then npx prettier --write "$file_path"; fi; }" \n          } \n        ] \n      } \n    ] \n  }\n}
\n\n

Example 2: Auto-fix Markdown Formatting

\n\n

Step 1: Add Hook Configuration

\n\n
{ \n  "hooks": { \n    "PostToolUse": [ \n      { \n        "matcher": "Edit|Write", \n        "hooks": [ \n          { \n            "type": "command", \n            "command": ""$CLAUDE_PROJECT_DIR"/.claude/hooks/markdown_formatter.py" \n          } \n        ] \n      } \n    ] \n  }\n}
\n\n

Step 2: Create Formatter Script

\n\n

Create .claude/hooks/markdown_formatter.py:

\n\n
## Example\n\n#!/usr/bin/env python3\n\n"""\nMarkdown formatter: Auto-complete code block language tags, clean extra blank lines\n"""\n\nimport json\nimport sys\nimport re\nimport os\n\ndef detect_language(code):\n    code = code.strip()\n    if re.search(r'^s*[{[]', code):\n        try: json.loads(code); return 'json'\n        except: pass\n    if re.search(r'^s*defs+w+s*$', code, re.M) or re.search(r'^s*(import|from)s+w+', code, re.M):\n        return 'python'\n    if re.search(r'b(functions+w+s*$|consts+w+s*=)', code) or re.search(r'=>|console.(log|error)', code):\n        return 'javascript'\n    if re.search(r'^#!.*b(bash|sh)b', code, re.M) or re.search(r'b(if|then|fi|for|in|do|done)b', code):\n        return 'bash'\n    return 'text'\n\ndef format_markdown(content):\n    fence_pattern = r'(?ms)^({0,3})```([^n]*)n(.*?)(n1```)s*$'\n    def add_lang(match):\n        indent, info, body, closing = match.groups()\n        if not info.strip():\n            lang = detect_language(body)\n            return f"{indent}```{lang}n{body}{closing}n"\n        return match.group(0)\n    content = re.sub(fence_pattern, add_lang, content)\n    content = re.sub(r'n{3,}', 'nn', content)\n    return content.rstrip() + 'n'\n\nif __name__ == "__main__":\n    try:\n        input_data = json.load(sys.stdin)\n        file_path = input_data.get('tool_input', {}).get('file_path', '')\n        if not file_path.endswith(('.md', '.mdx')): sys.exit(0)\n        if os.path.exists(file_path):\n            with open(file_path, 'r', encoding='utf-8') as f:\n                content = f.read()\n            formatted_content = format_markdown(content)\n            if formatted_content != content:\n                with open(file_path, 'w', encoding='utf-8') as f:\n                    f.write(formatted_content)\n                print(f"Formatted Markdown file: {file_path}")\n    except Exception as e:\n        print(f"Formatting failed: {e}", file=sys.stderr)\n        sys.exit(1)
\n\n

Step 3: Grant Execution Permission

\n\n
chmod +x .claude/hooks/markdown_formatter.py
\n\n

Example 3: Desktop Notification on User Input Required

\n\n

Send desktop alert when Claude waits for input (Linux/macOS):

\n\n
{ \n  "hooks": { \n    "Notification": [ \n      { \n        "matcher": "", \n        "hooks": [ \n          { \n            "type": "command", \n            "command": "notify-send 'Claude Code Alert' 'Please provide instructions or confirm permissions'" \n          } \n        ] \n      } \n    ] \n  }\n}
\n\n

Example 4: Block Sensitive File Modifications

\n\n

Prevent editing .env, package-lock.json, etc.:

\n\n
{ \n  "hooks": { \n    "PreToolUse": [ \n      { \n        "matcher": "Edit|Write", \n        "hooks": [ \n          { \n            "type": "command", \n            "command": "python3 -c "import json, sys; data=json.load(sys.stdin); path=data.get('tool_input',{}).get('file_path',''); sys.exit(2 if any(p in path for p in ['.env', 'package-lock.json', '.git/']) else 0)"" \n          } \n        ] \n      } \n    ] \n  }\n}
\n\n

Note: Exit code 2 blocks tool execution.

\n\n
\n\n

Claude Code Hooks Reference Manual

\n\n

Configuration Paths

\n\n\n \n \n \n \n \n
ScopeFile PathScope
User-level~/.claude/settings.jsonAll projects
Project-level.claude/settings.jsonCurrent project
Local project (non-committed).claude/settings.local.jsonCurrent project, not version-controlled
Managed policyAdmin-specified pathEnterprise/team-wide control
\n\n

Core Configuration Structure

\n\n

Hooks are organized by event+matcher, supporting command (Shell) and prompt (LLM) types:

\n\n
{ \n  "hooks": { \n    "【Event Name】": [ \n      { \n        "matcher": "【Tool matching rule】", \n        "hooks": [ \n          { \n            "type": "command/prompt", \n            "command": "【Shell command】", \n            "prompt": "【LLM prompt】", \n            "timeout": 30 \n          } \n        ] \n      } \n    ] \n  }\n}
\n\n

Matcher Rules (Tool Events Only)

\n\n\n \n \n \n \n \n
RuleExampleDescription
Exact matchWriteMatches only Write tool
Multiple toolsEdit|WriteMatches Edit or Write
Prefix matchNotebook.*Matches tools starting with Notebook
Wildcard* / emptyMatches all tools
\n\n

Advanced Configuration Techniques

\n\n\n \n \n \n \n
ScenarioConfigurationExample
Project scriptsUse $CLAUDE_PROJECT_DIR"command": ""$CLAUDE_PROJECT_DIR"/.claude/hooks/check-style.sh"
Plugin hooksConfigure hooks/hooks.json in plugin, use ${CLAUDE_PLUGIN_ROOT}"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh"
Component-level hooks (Skill/Agent)Define in component frontmatter, scope limited to component lifecycleSee Extended Configuration table
\n\n

Extended Configuration (Skill/Agent/Slash Commands)

\n\n

Component-embedded hooks only activate when corresponding component runs, automatically cleaned after execution.

\n\n

Supported Hook Events

\n\n

Only PreToolUse, PostTool

← Claude Code ControlClaude Code Plugins β†’