Claude Code Hooks | Beginner's Tutorial
\n\nClaude Code hooks are user-defined Shell commands that automatically execute at specific nodes in the Claude Code lifecycle.
\n\nWith 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\nTypical Application Scenarios
\n\nHooks 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
prettierafter editing.tsfiles, executegofmtafter modifying.gofiles \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
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\nImportant Security Reminder
\n\nWhen 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
- Review command logic and permissions line-by-line before registering hooks \n
- Avoid executing unknown scripts in hooks \n
- See official documentation Security Considerations for best practices \n
\n\n
Hook Event Types
\n\nClaude Code provides multiple lifecycle events to bind hook commands. Each event passes different context data and affects Claude's behavior differently.
\n\n| Event Name | Trigger Timing | Core Function |
|---|---|---|
PreToolUse | Before tool execution | Intercept tool execution (e.g., block sensitive file modifications), provide adjustment suggestions to Claude |
PermissionRequest | When permission dialog appears | Automatically approve/reject permission requests |
PostToolUse | After tool execution | Perform post-processing (e.g., code formatting, logging) |
UserPromptSubmit | After user submits prompt, before Claude processes | Pre-process user input (e.g., add context information) |
Notification | When Claude sends notifications | Customize notification methods (e.g., desktop popups, SMS alerts) |
Stop | When Claude completes response | Perform cleanup tasks (e.g., delete temporary files) |
SubagentStop | When sub-agent tasks complete | Process sub-agent execution results |
PreCompact | Before context compression | Customize compression rules |
SessionStart | When starting/restoring a session | Initialize session environment (e.g., load project configurations) |
SessionEnd | When session ends | Save session data, clean up environment |
\n\n
Quick Start: Implement Command Logging
\n\nThis example demonstrates configuring hooks to record all Bash commands executed by Claude.
\n\nPrerequisites
\n\nInstall jq (for JSON data parsing):
- \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
Step 1: Open Hook Configuration
\n\nIn Claude Code interface, enter /hooks, select event PreToolUse (triggers before tool execution, suitable for command logging).
Step 2: Add Event Matcher
\n\nMatchers define trigger conditions. To log only Bash commands:
\n\n- \n
- Select
+ Add new matcherβ¦\n - Enter keyword
Bash(triggers only when Bash tool is called) \n
* to match all tools for global hooks\n\nStep 3: Add Hook Command
\n\nSelect + Add new hookβ¦, enter command:
jq -r '"(.tool_input.command) - (.tool_input.description // "No description")"' >> ~/.claude/bash-command-log.txt\n\nCommand 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
Step 4: Choose Configuration Scope
\n\nConfiguration 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
Select User settings for global logging. Press Esc to exit configuration.
Step 5: Verify Hook Configuration
\n\n- \n
- Enter
/hooksto view configured hooks \n - Check configuration file
~/.claude/settings.json: \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\nStep 6: Test Hook Effectiveness
\n\n- \n
- Instruct Claude:
Help me execute the ls command(ask to execute ls command) \n - Check log file:
cat ~/.claude/bash-command-log.txt\n - If log shows:
ls - Lists files and directories, configuration succeeded \n
\n\n
Practical Hook Examples
\n\nCommon hook configurations you can copy directly or modify as needed.
\n\n> ? Full examples: Bash Command Validator Example\n\nExample 1: Auto-format TypeScript Files
\n\nAuto-format .ts files after editing/writing:
{ \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\nExample 2: Auto-fix Markdown Formatting
\n\nStep 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\nStep 2: Create Formatter Script
\n\nCreate .claude/hooks/markdown_formatter.py:
## 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\nStep 3: Grant Execution Permission
\n\nchmod +x .claude/hooks/markdown_formatter.py\n\nExample 3: Desktop Notification on User Input Required
\n\nSend 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\nExample 4: Block Sensitive File Modifications
\n\nPrevent editing .env, package-lock.json, etc.:
{ \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\nNote: Exit code 2 blocks tool execution.
\n\n\n\n
Claude Code Hooks Reference Manual
\n\nConfiguration Paths
\n\n| Scope | File Path | Scope |
|---|---|---|
| User-level | ~/.claude/settings.json | All projects |
| Project-level | .claude/settings.json | Current project |
| Local project (non-committed) | .claude/settings.local.json | Current project, not version-controlled |
| Managed policy | Admin-specified path | Enterprise/team-wide control |
Core Configuration Structure
\n\nHooks are organized by event+matcher, supporting command (Shell) and prompt (LLM) types:
{ \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\nMatcher Rules (Tool Events Only)
\n\n| Rule | Example | Description |
|---|---|---|
| Exact match | Write | Matches only Write tool |
| Multiple tools | Edit|Write | Matches Edit or Write |
| Prefix match | Notebook.* | Matches tools starting with Notebook |
| Wildcard | * / empty | Matches all tools |
Advanced Configuration Techniques
\n\n| Scenario | Configuration | Example |
|---|---|---|
| Project scripts | Use $CLAUDE_PROJECT_DIR | "command": ""$CLAUDE_PROJECT_DIR"/.claude/hooks/check-style.sh" |
| Plugin hooks | Configure 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 lifecycle | See Extended Configuration table |
Extended Configuration (Skill/Agent/Slash Commands)
\n\nComponent-embedded hooks only activate when corresponding component runs, automatically cleaned after execution.
\n\nSupported Hook Events
\n\nOnly PreToolUse, PostTool
YouTip