为 AI 编码助手构建安全护栏:Claude Code 的 PreToolUse Hook 系统

发布: (2026年1月19日 GMT+8 06:09)
10 min read
原文: Dev.to

Source: Dev.to

请提供您希望翻译的文章正文内容(包括任何 Markdown 格式),我将为您翻译成简体中文并保留代码块、URL 和技术术语不变。

AI 编码助手 vs. 项目规则

AI 编码助手能够快速实现代码,但它们遵循的是训练时的默认设置,而不是你的项目规则。它们并不知道你的团队:

  • 禁止 git reset --hard
  • 要求 GPG‑签名的提交
  • 使用的 GitHub 组织与用户的用户名不同

Claude Code 的 PreToolUse 钩子系统在工具调用执行前进行拦截,阻止危险操作,并注入上下文指导。本文将演示一个由四个专用钩子组成的生产实现。

版本说明

本文使用的 additionalContext 功能需要 Claude Code v2.1.9(2026 年 1 月 16 日发布)或更高版本。早期版本支持阻塞钩子,但不支持上下文注入。此功能来源于 feature request #15345

钩子合约

PreToolUse 钩子是一个可执行程序,它通过 stdin 接收 JSON,并通过退出码以及 stdout/stderr 与调用方通信:

{
  "tool_name": "Bash",
  "tool_input": { "command": "git reset --hard HEAD~3" }
}
退出代码含义输出
0允许stdout 将被解析以获取 additionalContext参见下文 “带上下文的允许” 格式
2阻止stderr 将显示给用户

带上下文的允许输出格式

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "additionalContext": "Your reminder or context here"
  }
}

Hook 1 – 安全防护

第一个钩子在破坏性操作执行之前进行阻止。

BLOCKING_RULES: list[SafetyRule] = [
    SafetyRule(
        pattern=r"git\s+reset\s+--hard",
        action="block",
        message=(
            "BLOCKED: Use `git stash` or `git rebase` instead. "
            "`git reset --hard` destroys uncommitted work."
        ),
    ),
    SafetyRule(
        pattern=r"--no-gpg-sign",
        action="block",
        message="BLOCKED: GPG signing is mandatory. Fix GPG issues, don't bypass.",
    ),
    SafetyRule(
        pattern=r"git\s+(?:add\s+)?(?:-A|--all|\.\s*$)",
        action="block",
        message=(
            "BLOCKED: Explicitly stage files to avoid committing unwanted files."
        ),
    ),
]

Why?
当要求提交更改时,Claude 往往会运行 git add -A 将所有内容暂存。在包含构建产物或 .env 文件的仓库中,这会把本不该提交的文件也暂存进去。强制显式暂存可以确保提交是有意的。

警告规则(在上下文中允许)

WARNING_RULES: list[SafetyRule] = [
    SafetyRule(
        pattern=r"git\s+push\s+--force(?!\s*-with-lease)",
        action="warn",
        message="WARNING: Use `--force-with-lease` for safer force push.",
    ),
    SafetyRule(
        pattern=r"\.env",
        action="warn",
        message="WARNING: This file may contain secrets. Never commit .env files.",
    ),
]

决策逻辑

def check_command(command: str) -> tuple[str, str]:
    # Check blocking rules first
    for rule in BLOCKING_RULES:
        if rule.matches(command):
            return ("block", rule.message)

    # Then warning rules
    for rule in WARNING_RULES:
        if rule.matches(command):
            return ("warn", rule.message)

    # Default: allow
    return ("allow", "")

Hook 2 – 为 GitHub API 调用注入上下文

仓库路径并不总是与 GitHub 组织名称匹配。

  • 本地检出~/dev/AcmeCorp/webapp
  • GitHub 所有者AcmeCorp(而不是用户 jdoe)。

如果不进行干预,Claude 会默认使用错误的所有者进行 API 调用。此 Hook 会在 Claude 使用 GitHub MCP 工具时注入仓库元数据。

GITHUB_BASE_CONTEXT = """CONTEXT: This repo's GitHub owner is 'AcmeCorp', NOT 'jdoe'.
Always use owner='AcmeCorp' in GitHub API calls.
Repository name: webapp
Default branch: main"""

PROJECT_BOARD_CONTEXT = """
Project board ID: PVT_xxxxxxxxxxxxxxxxxxxx
Epic Priority field ID: PVTF_xxxxxxxxxxxxxxxxxxxxxxx
Status field ID: PVTSSF_xxxxxxxxxxxxxxxxxxxxxxx

Note: gh project item-edit silently fails for Number fields. Use GraphQL mutations instead."""

Hook Matcher

{
  "matcher": "mcp__github__.*",
  "hooks": [
    { "type": "command", "command": "python3 /path/to/context-injection.py" }
  ]
}

与项目相关的工具同样会收到额外的 board‑ID 上下文。

Hook 3 – 工作流规则强制

某些工作流规则无法通过 linter 强制执行(例如 TDD 纪律、worktree 使用、提交签名)。此钩子会在相关时机弹出提醒,但不会阻塞操作。

GPG_REMINDER = WorkflowReminder(
    message="GPG REMINDER: Never use `--no-gpg-sign`. If GPG fails, check sandbox mode."
)

TDD_REMINDER = WorkflowReminder(
    message=(
        "TDD REMINDER: Is there a failing test? Write the RED test first, "
        "then write minimal code to make it GREEN."
    )
)

WORKTREE_REMINDER = WorkflowReminder(
    message=(
        "WORKTREE REMINDER: You appear to be working directly on main. "
        "Use `git worktree add .worktrees/issue-XX-description -b issue-XX-description`"
    )
)

提醒注入逻辑

def get_write_edit_reminders(file_path: str) -> list[str]:
    reminders = []

    # TDD reminder for Go production code
    if is_go_production_code(file_path):
        reminders.append(TDD_REMINDER.message)

    # Worktree reminder if editing production code outside a worktree
    if not is_in_worktree() and is_production_code_path(file_path):
        reminders.append(WORKTREE_REMINDER.message)

    return reminders

Hook 4 –(可选)自定义扩展

您可以为以下功能添加更多 hook:

  • Secret scanning – 当命令引用已知的密钥模式时,阻止或发出警告。
  • Dependency checks – 在运行 npm install 之前注入关于已批准的包版本的上下文。
  • CI/CD gating – 在允许 git push 之前,需要一次成功的本地测试运行。

每个新 hook 都遵循相同的约定:从 stdin 读取 JSON,依据退出码作出决定,并可选地返回 additionalContext

摘要

通过层叠这四个 PreToolUse 钩子,你可以获得:

钩子主要目标结果
安全防护阻止破坏性命令,对风险模式发出警告防止数据丢失,提示最佳实践警告
GitHub 上下文注入使 Claude 的 API 调用与实际仓库元数据保持一致纠正所有者/看板 ID,避免错误的 API 调用
工作流规则强制执行提醒开发者团队特定的流程在不阻塞的情况下强化 TDD、工作树使用、GPG 签名
自定义扩展针对额外的项目约束进行定制灵活且面向未来的强制执行

实现此钩子链后,Claude Code 能够遵守贵组织的政策,同时仍然提供你对 AI 编码助手的快速帮助期望。

reminders.append(WORKTREE_REMINDER.message)

return reminders

工作树检查

工作树检查使用环境变量来检测当前目录:

def is_in_worktree() -> bool:
    cwd = os.environ.get("PWD", os.getcwd())
    return ".worktrees/" in cwd

Hook 4:技能建议引擎

Claude Code 支持 skills(针对特定领域的可复用指令集)。技能建议钩子将文件模式映射到相应的技能,提示 Claude 在继续之前加载领域特定的指导。

SKILL_TRIGGERS: list[SkillTrigger] = [
    # Test files need test classification + TDD patterns
    SkillTrigger(
        skills=("classifying-test-sizes", "developing-with-tdd"),
        path_contains="_test.go",
    ),
    # Temporal workflows need TDD + Temporal + Go monorepo skills
    SkillTrigger(
        skills=("developing-with-tdd", "developing-with-temporal", "working-with-go-monorepo"),
        path_contains="apps/workers/",
        extension=".go",
    ),
    # Database migrations
    SkillTrigger(
        skills=("working-with-atlas-migrations",),
        path_contains="db/migrations/",
    ),
]

触发器列表采用 先匹配先执行 的顺序。特定模式(测试文件、迁移文件)放在通用模式(apps/ 中的任意 Go 文件)之前。这确保 _test.go 文件会触发针对测试的技能,而不是通用的 Go 指导。

输出会提醒 Claude 调用相应的技能:

if skills:
    skill_list = ", ".join(f"'{s}'" for s in skills)
    output = {
        "hookSpecificOutput": {
            "additionalContext": (
                f"SKILL REMINDER: Before proceeding, invoke these skills: {skill_list}. "
                f"They contain patterns, gotchas, and best practices for this file type."
            )
        }
    }

Hook Configuration

settings.json 文件负责调度哪些钩子在何种工具上运行:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Write|Edit",
        "hooks": [{ "type": "command", "command": "python3 .../suggest-skills.py" }]
      },
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "python3 .../safety-guards.py" },
          { "type": "command", "command": "python3 .../workflow-rules.py" }
        ]
      },
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": "python3 .../workflow-rules.py" }]
      },
      {
        "matcher": "mcp__github__.*",
        "hooks": [{ "type": "command", "command": "python3 .../context-injection.py" }]
      }
    ]
  }
}
  • matcher 字段接受正则表达式模式。
  • Bash 命令会同时收到 安全防护工作流规则
  • 写入/编辑操作会触发工作流规则 以及 技能建议。
  • GitHub MCP 工具会收到上下文注入。

观察

  • Claude 不再尝试 git reset --hard。安全钩子阻止了它并提供了替代方案。
  • GitHub API 调用默认使用正确的所有者。在上下文钩子之前,我大约在 五分之一 的 API 调用中手动纠正了所有者。
  • TDD 提醒会在每次编辑 Go 文件时出现。我无法量化合规性,但该提醒使跳过红‑test 步骤显得是有意为之,而非偶然。
  • 当我编辑工作流文件时,技能加载会自动进行,而不需要我记得去调用它们。

实现注意事项

  • Hook 不能修改工具输入;它们只能 allow(允许)block(阻止)inject context(注入上下文)
  • 需要多次文件读取的复杂验证会减慢每一次工具调用。
  • Hook 只能看到 individual tool calls(单个工具调用),而看不到更广泛的对话上下文。
  • 错误处理默认宽松:JSON‑parse 失败会返回退出码 0(allow,允许),而不是阻止操作。这可以防止因 Hook 损坏而完全中断开发工作流。

完整实现由四个 Python 文件组成,总计 400 行以下。每个 Hook 只承担单一职责,便于测试和修改。添加新需求只需新增 Hook,保持每个 Hook 的聚焦性。

Back to Blog

相关文章

阅读更多 »

学习逆向黑客以及它为何有效

逆向学习用于黑客 当你坐下来时,terminal 已经打开。没有 splash screen。没有 tutorial。只有一个闪烁的 cursor 和机器的微弱嗡嗡声。