为 AI 编码助手构建安全护栏:Claude Code 的 PreToolUse Hook 系统
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 的聚焦性。