首页 › tutorials › Claude Code Hooks深度实战:13个事件全解析

概述

本教程深入剖析Claude Code Hooks系统的完整事件模型、JSON输入输出协议、退出码语义和Matcher过滤机制。通过12个生命周期事件的逐一拆解和6个生产级实战案例,帮助开发者构建安全护栏、自动格式化、上下文注入、会话管理等能力。适合已具备Claude Code基础使用经验、希望将开发流程自动化到极致的中高级开发者。

前置条件


第一章:Hooks架构全景——理解事件驱动模型

Claude Code的Hooks系统本质上是一个事件驱动的中间件层。每当Claude Code执行工具调用、用户提交prompt、会话启动或结束时,都会触发预注册的hook脚本。这些脚本可以拦截、修改、阻止或记录每一个操作。

事件生命周期总览

图表1

图表 1

完整事件清单

Claude Code支持13个hook事件,分为三大类:

类别 事件 触发时机 Matcher支持
工具生命周期 PreToolUse 工具执行前 正则匹配tool_name
PostToolUse 工具执行后 正则匹配tool_name
PostToolUseFailure 工具执行失败后 正则匹配tool_name
会话生命周期 SessionStart 会话启动/恢复 不支持
SessionEnd 会话结束 不支持
Setup 首次配置 不支持
Stop 主Agent完成回复 不支持
SubagentStart 子Agent启动 不支持
SubagentStop 子Agent完成 不支持
用户交互 UserPromptSubmit 用户提交prompt 不支持
Notification 系统通知(权限请求等) 不支持
PermissionRequest 权限请求 不支持
上下文管理 PreCompact 对话压缩前 不支持

配置文件层级

Hooks可以配置在三个层级,后加载的覆盖先加载的:

~/.claude/settings.json          # 全局配置,所有项目生效
项目/.claude/settings.json       # 项目级配置,团队共享
项目/.claude/settings.local.json # 本地配置,不提交到Git

第二章:JSON协议详解——stdin输入与stdout输出

每个hook脚本通过stdin接收JSON,通过stdout返回JSON,通过退出码传达决策。这是整个系统最核心的通信协议。

通用输入信封

所有hook事件共享的基础输入结构:

{
  "session_id": "abc123-def456",
  "transcript_path": "/tmp/claude-session-abc123.jsonl",
  "cwd": "/home/user/my-project",
  "hook_event_name": "PreToolUse"
}

退出码语义

退出码是hook控制Claude Code行为的核心手段:

退出码 含义 stdout行为 stderr行为
0 成功/放行 对UserPromptSubmit和SessionStart注入上下文,其他事件仅记录 忽略
2 阻止/拦截 忽略 路由到Claude或用户(见下表)
其他 非阻塞错误 忽略 显示给用户

Exit Code 2行为矩阵

图表2

图表 2

关键区别:stderr路由目标是Claude还是用户决定了你的错误消息应该写给谁看。当stderr发给Claude时,Claude会根据消息内容调整后续行为;发给用户时,仅作为显示信息。


第三章:PreToolUse——安全护栏的第一道防线

PreToolUse是使用频率最高的hook事件,它在Claude准备执行工具但还未执行时触发。你可以用它来阻止危险命令、保护敏感文件、执行预检查。

输入Schema

{
  "session_id": "abc123",
  "transcript_path": "/tmp/session.jsonl",
  "cwd": "/home/user/project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf node_modules",
    "description": "Clean node_modules"
  }
}

输出选项

推荐方式(hookSpecificOutput):

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "不允许删除node_modules,请使用 npm ci 重建"
  }
}

三种决策值: - "allow" — 绕过权限UI,直接执行(原因显示给用户) - "deny" — 阻止调用(原因显示给Claude,Claude会据此调整) - "ask" — 弹出权限确认UI(原因显示给用户)

Legacy方式(仍然支持):

{
  "decision": "block",
  "reason": "危险命令已被拦截"
}

Matcher过滤

只有PreToolUse和PostToolUse支持matcher字段,它是一个正则表达式,匹配tool_name:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/check_bash.py"
          }
        ]
      },
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/check_file_write.py"
          }
        ]
      }
    ]
  }
}

对于MCP工具,名称格式为mcp__<server>__<tool>,例如:

{
  "matcher": "mcp__filesystem__write_file",
  "hooks": [...]
}

空字符串""匹配所有工具。

实战案例1:阻止危险Bash命令

创建文件 .claude/hooks/pre_tool_use.py:

#!/usr/bin/env python3
"""PreToolUse Hook: 拦截危险的Bash命令和敏感文件访问"""

import json
import sys
import re
from pathlib import Path


def is_dangerous_rm(command: str) -> bool:
    """检测危险的rm命令模式"""
    normalized = ' '.join(command.lower().split())

    patterns = [
        r'\brm\s+.*-[a-z]*r[a-z]*f',     # rm -rf, rm -fr
        r'\brm\s+--recursive\s+--force',   # 长选项格式
        r'\brm\s+-r\s+.*-f',               # 分开的选项
    ]

    for pattern in patterns:
        if re.search(pattern, normalized):
            return True

    # rm -r 针对危险路径
    dangerous_paths = ['/', '/*', '~', '~/', '$HOME', '*', '.']
    if re.search(r'\brm\s+.*-[a-z]*r', normalized):
        for path in dangerous_paths:
            if re.search(re.escape(path), normalized):
                return True

    return False


def is_protected_file(tool_name: str, tool_input: dict) -> bool:
    """检查是否访问受保护的文件"""
    protected = ['.env', '.env.production', '.env.staging']

    if tool_name in ('Read', 'Edit', 'Write', 'MultiEdit'):
        file_path = tool_input.get('file_path', '')
        for p in protected:
            if p in file_path and not file_path.endswith('.env.sample'):
                return True

    return False


def main():
    try:
        data = json.load(sys.stdin)
        tool_name = data.get('tool_name', '')
        tool_input = data.get('tool_input', {})

        # 检查受保护文件
        if is_protected_file(tool_name, tool_input):
            msg = json.dumps({
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason":
                        "受保护的.env文件不可修改,请使用.env.sample作为模板"
                }
            })
            print(msg)
            sys.exit(0)

        # 检查危险Bash命令
        if tool_name == 'Bash':
            command = tool_input.get('command', '')
            if is_dangerous_rm(command):
                msg = json.dumps({
                    "hookSpecificOutput": {
                        "hookEventName": "PreToolUse",
                        "permissionDecision": "deny",
                        "permissionDecisionReason":
                            f"检测到危险删除命令: '{command[:80]}'。已被安全策略拦截。"
                    }
                })
                print(msg)
                sys.exit(0)

        sys.exit(0)  # 放行

    except Exception:
        sys.exit(0)  # 异常时放行,不阻塞工作流


if __name__ == '__main__':
    main()

在.claude/settings.json中注册:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/pre_tool_use.py"
          }
        ]
      }
    ]
  }
}

预期效果: 当Claude尝试执行rm -rf /或编辑.env文件时,命令会被阻止,Claude会收到原因说明并调整行为。


第四章:PostToolUse——自动格式化与结果验证

PostToolUse在工具成功执行后触发。它无法阻止已发生的操作,但可以: - 对工具结果给出自动反馈 - 触发后续自动化(格式化、lint、测试) - 记录审计日志

输入Schema

{
  "session_id": "abc123",
  "hook_event_name": "PostToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/home/user/project/src/app.tsx",
    "content": "export default function App() { return <div>Hello</div> }"
  },
  "tool_response": {
    "success": true
  }
}

实战案例2:自动格式化写入的代码

#!/usr/bin/env python3
"""PostToolUse Hook: 自动格式化编辑过的文件"""

import json
import sys
import subprocess
from pathlib import Path


# 需要自动格式化的文件扩展名和对应命令
FORMATTERS = {
    '.ts': 'npx prettier --write {path}',
    '.tsx': 'npx prettier --write {path}',
    '.js': 'npx prettier --write {path}',
    '.jsx': 'npx prettier --write {path}',
    '.css': 'npx prettier --write {path}',
    '.json': 'npx prettier --write {path}',
    '.py': 'python3 -m black {path}',
    '.go': 'gofmt -w {path}',
    '.rs': 'rustfmt {path}',
}


def should_format(tool_name: str, tool_input: dict) -> str | None:
    """判断是否需要格式化,返回文件路径或None"""
    if tool_name not in ('Write', 'Edit', 'MultiEdit'):
        return None

    file_path = tool_input.get('file_path', '')
    suffix = Path(file_path).suffix

    if suffix in FORMATTERS:
        return file_path
    return None


def main():
    try:
        data = json.load(sys.stdin)
        tool_name = data.get('tool_name', '')
        tool_input = data.get('tool_input', {})

        file_path = should_format(tool_name, tool_input)
        if not file_path:
            sys.exit(0)

        suffix = Path(file_path).suffix
        cmd = FORMATTERS[suffix].format(path=file_path)

        result = subprocess.run(
            cmd, shell=True, capture_output=True, text=True, timeout=30
        )

        if result.returncode == 0:
            # 格式化成功,给Claude一个反馈
            output = json.dumps({
                "decision": None,
                "reason": f"已自动格式化 {file_path}"
            })
            print(output)
        else:
            # 格式化失败,让Claude知道
            output = json.dumps({
                "decision": "block",
                "reason": f"格式化失败: {result.stderr[:200]}"
            })
            print(output)

        sys.exit(0)

    except Exception as e:
        print(f"格式化hook异常: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()

配置Matcher精确匹配文件修改工具:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/auto_format.py"
          }
        ]
      }
    ]
  }
}

第五章:UserPromptSubmit——上下文注入与Prompt预处理

UserPromptSubmit在用户按下Enter提交prompt之前触发。它有两个独特能力: 1. 阻止prompt:可以拦截不合适的输入 2. 注入上下文:通过stdout向Claude的上下文中追加额外信息

输入Schema

{
  "session_id": "abc123",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "帮我删除所有node_modules文件夹"
}

上下文注入机制

这是UserPromptSubmit最强大的特性。当hook以exit code 0退出时,stdout内容会被注入到Claude的上下文中,相当于用户在prompt后面附加了额外信息,但用户自己看不到。

实战案例3:自动附加Git上下文

#!/usr/bin/env python3
"""UserPromptSubmit Hook: 自动注入当前分支和最近改动信息"""

import json
import sys
import subprocess


def get_git_context() -> str:
    """获取Git仓库当前状态"""
    parts = []

    # 当前分支
    result = subprocess.run(
        ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
        capture_output=True, text=True, timeout=5
    )
    if result.returncode == 0:
        branch = result.stdout.strip()
        parts.append(f"当前Git分支: {branch}")

    # 未提交的改动数量
    result = subprocess.run(
        ['git', 'status', '--porcelain'],
        capture_output=True, text=True, timeout=5
    )
    if result.returncode == 0:
        changes = [l for l in result.stdout.strip().split('\n') if l]
        if changes:
            parts.append(f"未提交的文件改动: {len(changes)}个")
            # 列出前5个改动文件
            for change in changes[:5]:
                parts.append(f"  {change}")

    # 最近3条commit
    result = subprocess.run(
        ['git', 'log', '--oneline', '-3'],
        capture_output=True, text=True, timeout=5
    )
    if result.returncode == 0 and result.stdout.strip():
        parts.append("最近提交:")
        for line in result.stdout.strip().split('\n'):
            parts.append(f"  {line}")

    return '\n'.join(parts) if parts else ""


def main():
    try:
        data = json.load(sys.stdin)
        prompt = data.get('prompt', '')

        # 检测到关键词时注入额外上下文
        keywords = ['提交', 'commit', '发布', 'deploy', 'push', '测试', 'test']
        should_inject = any(kw in prompt.lower() for kw in keywords)

        if should_inject:
            context = get_git_context()
            if context:
                # stdout内容会被注入到上下文
                print(f"\n[自动上下文]\n{context}\n")

        sys.exit(0)

    except Exception:
        sys.exit(0)


if __name__ == '__main__':
    main()

第六章:SessionStart——会话引导与环境初始化

SessionStart在每次启动或恢复Claude Code会话时触发。与UserPromptSubmit类似,它的stdout(exit code 0时)会注入到上下文中,这使它成为会话引导的绝佳工具。

输入Schema

{
  "session_id": "abc123",
  "hook_event_name": "SessionStart",
  "source": "startup",   // "startup" | "resume" | "clear"
  "cwd": "/home/user/project"
}

实战案例4:根据项目类型注入引导上下文

#!/usr/bin/env python3
"""SessionStart Hook: 根据项目结构自动注入引导信息"""

import json
import sys
from pathlib import Path


def detect_project_type(cwd: str) -> dict:
    """检测项目类型和关键信息"""
    root = Path(cwd)
    info = {"type": "unknown", "tips": []}

    # Node.js / React 项目
    if (root / 'package.json').exists():
        info["type"] = "node"
        pkg = json.loads((root / 'package.json').read_text())
        info["name"] = pkg.get('name', 'unknown')

        scripts = pkg.get('scripts', {})
        if 'test' in scripts:
            info["tips"].append(f"测试命令: npm test ({scripts['test']})")
        if 'build' in scripts:
            info["tips"].append(f"构建命令: npm run build ({scripts['build']})")
        if 'lint' in scripts:
            info["tips"].append(f"代码检查: npm run lint ({scripts['lint']})")

        deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
        if 'react' in deps:
            info["framework"] = f"React {deps['react']}"
        if 'next' in deps:
            info["framework"] = f"Next.js {deps['next']}"
        if 'vue' in deps:
            info["framework"] = f"Vue {deps['vue']}"

    # Python 项目
    elif (root / 'pyproject.toml').exists() or (root / 'setup.py').exists():
        info["type"] = "python"
        if (root / 'pyproject.toml').exists():
            info["tips"].append("使用 uv 管理依赖")
        if (root / 'pytest.ini').exists() or (root / 'conftest.py').exists():
            info["tips"].append("测试命令: pytest")

    # Go 项目
    elif (root / 'go.mod').exists():
        info["type"] = "go"
        info["tips"].extend([
            "构建: go build ./...",
            "测试: go test ./...",
            "格式化: gofmt -w ."
        ])

    # Rust 项目
    elif (root / 'Cargo.toml').exists():
        info["type"] = "rust"
        info["tips"].extend([
            "构建: cargo build",
            "测试: cargo test",
            "格式化: cargo fmt"
        ])

    # 检测CLAUDE.md
    if (root / 'CLAUDE.md').exists():
        info["tips"].append("已检测到CLAUDE.md项目配置文件")

    return info


def main():
    try:
        data = json.load(sys.stdin)
        cwd = data.get('cwd', '.')
        source = data.get('source', 'startup')

        if source == 'startup':
            info = detect_project_type(cwd)

            lines = ["[项目自动识别]"]
            lines.append(f"项目类型: {info['type']}")

            if 'framework' in info:
                lines.append(f"框架: {info['framework']}")
            if 'name' in info:
                lines.append(f"项目名: {info['name']}")

            if info['tips']:
                lines.append("可用命令:")
                for tip in info['tips']:
                    lines.append(f"  - {tip}")

            print('\n'.join(lines))

        sys.exit(0)

    except Exception:
        sys.exit(0)


if __name__ == '__main__':
    main()

效果: 每次启动Claude Code时,Claude会自动"看到"项目的技术栈和可用命令,无需手动在CLAUDE.md中重复配置。


第七章:Stop与SubagentStop——防止过早结束

Stop事件在主Agent完成回复时触发,SubagentStop在子Agent(通过Task工具派发)完成时触发。通过返回exit code 2或decision: "block",你可以阻止Agent停止,让它继续工作。

输入Schema

{
  "session_id": "abc123",
  "hook_event_name": "Stop",
  "stop_hook_active": false
}

实战案例5:确保代码通过所有检查后才能停止

#!/usr/bin/env python3
"""Stop Hook: 确保所有改动通过lint和测试后才允许结束"""

import json
import sys
import subprocess
from pathlib import Path


def has_uncommitted_changes() -> bool:
    result = subprocess.run(
        ['git', 'diff', '--exit-code'],
        capture_output=True, timeout=10
    )
    return result.returncode != 0


def run_lint() -> tuple[bool, str]:
    """运行lint检查"""
    root = Path.cwd()

    if (root / 'package.json').exists():
        result = subprocess.run(
            ['npx', 'eslint', '.', '--max-warnings=0'],
            capture_output=True, text=True, timeout=60
        )
        if result.returncode != 0:
            return False, result.stdout + result.stderr

    if (root / 'pyproject.toml').exists():
        result = subprocess.run(
            ['python3', '-m', 'ruff', 'check', '.'],
            capture_output=True, text=True, timeout=60
        )
        if result.returncode != 0:
            return False, result.stdout + result.stderr

    return True, ""


def run_tests() -> tuple[bool, str]:
    """运行测试"""
    root = Path.cwd()

    if (root / 'package.json').exists():
        result = subprocess.run(
            ['npm', 'test'], capture_output=True, text=True, timeout=120
        )
        if result.returncode != 0:
            return False, result.stdout[-500:]  # 最后500字符

    return True, ""


def main():
    try:
        data = json.load(sys.stdin)

        if not has_uncommitted_changes():
            sys.exit(0)  # 没有改动,直接放行

        # 有未提交改动,执行检查
        lint_ok, lint_err = run_lint()
        if not lint_ok:
            output = json.dumps({
                "decision": "block",
                "reason": f"代码Lint检查未通过,请修复后再完成:\n{lint_err[:500]}"
            })
            print(output)
            sys.exit(0)

        test_ok, test_err = run_tests()
        if not test_ok:
            output = json.dumps({
                "decision": "block",
                "reason": f"测试未通过,请修复后再完成:\n{test_err[:500]}"
            })
            print(output)
            sys.exit(0)

        sys.exit(0)  # 全部通过

    except Exception:
        sys.exit(0)


if __name__ == '__main__':
    main()

第八章:Notification与PreCompact——通知与上下文保护

Notification事件

在系统通知时触发(如等待权限确认、长时间无输入等)。一个常见用途是语音提醒或桌面通知。

#!/usr/bin/env python3
"""Notification Hook: 发送桌面通知"""

import json
import sys
import subprocess
import os


def send_desktop_notification(message: str):
    """发送桌面通知(Linux/macOS)"""
    if sys.platform == 'darwin':
        # macOS: 使用osascript
        script = f'display notification "{message}" with title "Claude Code"'
        subprocess.run(['osascript', '-e', script], timeout=5)
    else:
        # Linux: 使用notify-send
        subprocess.run(
            ['notify-send', 'Claude Code', message, '-t', '5000'],
            timeout=5
        )


def main():
    try:
        data = json.load(sys.stdin)
        message = data.get('message', '需要你的关注')
        send_desktop_notification(message)
        sys.exit(0)
    except Exception:
        sys.exit(0)


if __name__ == '__main__':
    main()

PreCompact事件

在对话压缩前触发(手动/compact或自动压缩)。可以用来备份对话记录或保存关键上下文。

#!/usr/bin/env python3
"""PreCompact Hook: 备份压缩前的对话记录"""

import json
import sys
import shutil
from pathlib import Path
from datetime import datetime


def main():
    try:
        data = json.load(sys.stdin)
        transcript_path = data.get('transcript_path', '')
        trigger = data.get('trigger', 'unknown')

        if transcript_path and Path(transcript_path).exists():
            # 创建备份目录
            backup_dir = Path('logs') / 'transcript_backups'
            backup_dir.mkdir(parents=True, exist_ok=True)

            # 生成带时间戳的备份文件名
            ts = datetime.now().strftime('%Y%m%d_%H%M%S')
            session = Path(transcript_path).stem
            backup = backup_dir / f"{session}_pre_compact_{trigger}_{ts}.jsonl"

            shutil.copy2(transcript_path, backup)

        sys.exit(0)
    except Exception:
        sys.exit(0)


if __name__ == '__main__':
    main()

第九章:实战项目——构建完整的安全与自动化Hooks套件

将前面所有案例整合为一个完整的项目级配置。

项目结构

my-project/
├── .claude/
│   ├── settings.json          # Hooks注册配置
│   ├── hooks/
│   │   ├── pre_tool_use.py    # 安全护栏
│   │   ├── post_tool_use.py   # 自动格式化
│   │   ├── user_prompt.py     # Prompt预处理
│   │   ├── session_start.py   # 会话引导
│   │   ├── stop.py            # 完成检查
│   │   ├── notification.py    # 桌面通知
│   │   └── pre_compact.py     # 上下文备份
│   └── data/
│       └── sessions/          # 会话数据存储
├── logs/                      # 审计日志(gitignore)
│   └── .gitkeep
└── .gitignore

完整settings.json

{
  "permissions": {
    "allow": [
      "Bash(npm:*)",
      "Bash(git:*)",
      "Bash(npx:*)",
      "Bash(python3:*)",
      "Bash(pytest:*)",
      "Read",
      "Glob",
      "Grep"
    ],
    "deny": []
  },
  "hooks": {
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/session_start.py"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash|Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/pre_tool_use.py"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/post_tool_use.py"
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/user_prompt.py"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/stop.py"
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/notification.py"
          }
        ]
      }
    ],
    "PreCompact": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/pre_compact.py"
          }
        ]
      }
    ]
  }
}

完整工作流

图表3

图表 3


第十章:调试技巧与常见问题

调试Hook执行

当hook行为不符合预期时,逐步排查:

1. 检查hook是否被加载

# 查看生效的settings配置
cat .claude/settings.json | python3 -m json.tool

2. 手动测试hook脚本

# 模拟PreToolUse输入
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | \
  python3 .claude/hooks/pre_tool_use.py
echo "Exit code: $?"

3. 添加日志输出

在hook脚本中添加日志记录:

import datetime

def log(msg: str):
    with open('logs/hooks_debug.log', 'a') as f:
        ts = datetime.datetime.now().isoformat()
        f.write(f"[{ts}] {msg}\n")

4. 理解stderr路由

关键规则: - PreToolUse exit 2 → stderr给Claude(Claude会读并调整) - UserPromptSubmit exit 2 → stderr给用户(Claude看不到) - Stop exit 2 → stderr给Claude(Claude继续工作)

常见陷阱

问题 原因 解决方案
Hook没有触发 Matcher写错了 检查tool_name大小写,用空字符串测试
stdout没注入上下文 只有UserPromptSubmit和SessionStart支持 用hookSpecificOutput.additionalContext
阻止了但Claude没看到原因 stderr路由到了用户而非Claude 检查事件类型,用JSON输出代替stderr
Hook超时 脚本执行时间过长 给subprocess加timeout,hook总时限约30秒
Python模块缺失 使用了第三方库 用uv run --script加inline依赖声明

使用uv管理Hook依赖

对于需要第三方库的hook脚本,使用uv的inline script metadata:

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
#     "python-dotenv",
#     "requests",
# ]
# ///

import requests
# ... 正常代码

然后在settings.json中用uv run调用:

{
  "type": "command",
  "command": "uv run .claude/hooks/notification.py"
}

第十一章:高级模式与最佳实践

模式1:多Matcher分层策略

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {"type": "command", "command": "python3 .claude/hooks/check_bash.py"}
      ]
    },
    {
      "matcher": "Write|Edit",
      "hooks": [
        {"type": "command", "command": "python3 .claude/hooks/check_file_write.py"}
      ]
    },
    {
      "matcher": "",
      "hooks": [
        {"type": "command", "command": "python3 .claude/hooks/log_all_tools.py"}
      ]
    }
  ]
}

空字符串Matcher匹配所有工具,放在最后作为兜底日志。

模式2:环境变量控制Hook行为

#!/usr/bin/env python3
import os

# 通过环境变量控制是否启用某个hook
ENABLE_STRICT_MODE = os.getenv('CLAUDE_HOOK_STRICT', 'false') == 'true'
ENABLE_NOTIFICATIONS = os.getenv('CLAUDE_HOOK_NOTIFY', 'true') == 'true'

if not ENABLE_STRICT_MODE:
    sys.exit(0)  # 跳过检查

在.bashrc或.zshrc中设置:

export CLAUDE_HOOK_STRICT=true    # 严格模式
export CLAUDE_HOOK_NOTIFY=true    # 开启通知

模式3:HTTP Hook类型

除了本地命令,hook还支持HTTP端点:

{
  "type": "http",
  "url": "https://your-server.com/api/clause-hook"
}

HTTP hook会收到POST请求,body是相同的JSON输入。服务器需要返回对应的JSON输出。这适合: - 集成企业内部审批系统 - 发送Webhook到Slack/Discord - 连接CI/CD系统

模式4:LLM Hook类型

Hook还可以调用LLM做智能决策:

{
  "type": "llm",
  "prompt": "分析这个Bash命令是否安全。如果涉及rm、curl pipe bash、sudo等危险操作,返回BLOCKED。"
}

第十二章:性能考量

Hook执行开销

每个hook的执行时间直接影响Claude Code的响应速度。实测数据:

Hook操作 典型耗时 优化建议
纯Python判断 <50ms 无需优化
Git命令 50-200ms 减少git调用次数
npm/npx命令 1-5s 仅在文件变更时触发
格式化单个文件 200ms-1s 使用matcher精确匹配
LLM调用 2-10s 仅在关键决策点使用

优化策略

  1. 精确Matcher:避免空字符串matcher运行重量级hook
  2. 快速退出:在hook脚本开头做快速判断,不需要处理时立即sys.exit(0)
  3. 缓存机制:对重复查询的结果做进程内缓存
  4. 异步操作:通知类hook放在Notification事件而非PostToolUse
  5. 条件触发:通过环境变量或文件标记控制hook是否激活

延伸阅读

训练营适用性评估

维度 评估
适合训练营类型 AI开发工具训练营、DevOps自动化训练营、企业AI治理训练营
预计教学时长 4-6小时(含动手实验)
难度等级 中高级(需要Python/Shell脚本和CLI基础)
实验环境要求 本地开发机 + Claude Code CLI + Git项目
建议分组 每人独立完成,最后小组分享各自创建的hook套件
前置课程 Claude Code基础使用、CLAUDE.md配置

参考来源

  1. Hooks Reference - Claude Code Docs — Anthropic官方hooks事件、配置schema、JSON输入输出格式权威参考
  2. Claude Code Hooks Mastery - GitHub — disler维护的完整生产级hooks示例项目,覆盖13个生命周期事件
  3. Claude Code Hooks: Events, Matchers & Exit Codes - CodingNomads — 事件分类、Matcher正则、退出码行为深度解析
  4. Hooks Input & Output Schemas - GitHub Gist — 实现级JSON Schema完整参考,含所有事件的输入输出结构
  5. Claude Code Hooks: PreToolUse & PostToolUse Tutorial — 以安全防护和自动格式化为核心的实战教程

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入