编程代理不守规则?用Hooks强制检查给AI编程兜底 看到一个让我很有共鸣的技术分享标题——《如果你的编程代理无视你的规则请观看此视频你需要钩子》。这个名字几乎概括了 AI 编程工具用户最普遍的一类困惑规则写了一大堆代理该违规还是违规。你放好了CLAUDE.md写清了AGENTS.md甚至把禁止事项加粗、编号、反复强调结果代理照样在修复 Bug 时留下console.log照样在提交信息里不写任务单号。问题不是规则写得太少而是规则本身只是“建议”不是“执行”。这篇文章想讲清楚的是规则为什么会被无视以及 hooks钩子为什么能真正兜底。我会用三个可以直接复制的示例来说明基于 Git 的提交校验、基于 pre-commit 的代码检查、以及基于代理工具的运行时拦截。读完你会得到一套判断“该用规则还是该用 hooks”的思维模型以及一份落地强制检查的执行清单。1. 这篇文章真正要解决的问题先还原一个高频开发场景。你在项目根目录放好了AGENTS.md里面清清楚楚写着禁止新增console.log和debugger提交信息必须以[TASK-编号]开头修改公共模块前必须补充测试。这份规则足够具体可当你让代理修复一个线上 Bug 时它照样在代码里塞了几条console.log来“调试”提交时又丢下一句 “fix: bug”。更麻烦的是这个问题不是个例。凡是基于“把规则写成文本、让模型自己遵守”的方案都有一个共同特点规则是被“阅读”的而不是被“执行”的。模型在生成代码时会参考这些文本但“参考”不等于“遵守”。上下文变长时规则会被遗忘需求描述更紧急时规则会被压过工具调用环节模型更不会停下来逐条核对。hooks 要解决的就是这个缺口。它的思路不是“让代理更好地记住规则”而是“在关键动作发生前后由外部程序替我们检查”。代理可以忘记规则外部检查脚本不会忘记代理可以判断失误退出码不会含糊。需要纠正一个常见误判很多人把“规则不生效”当成“规则没写清楚”于是不断加长规则最后规则变成几千字的文档模型更不愿意读。正确方向是把“希望代理自发做对”的事情交给规则文件把“必须由系统强制做对”的事情交给 hooks。谁最应该读这篇文章正在用 Claude Code、Cline、Cursor 等 AI 编程代理做实际项目但发现代理总在相同问题上反复违规的人已经出现“提交信息混乱、调试代码混入主分支”等问题的技术负责人想理解 AI 编程工具工程化落地方式的开发者。一句话总结如果你觉得编程代理“不听话”你需要的不是一个更长的规则文件而是一个让它无法绕过的检查点。2. 软约束与硬约束规则文件 vs Hooks在动手配置之前需要先建立两个核心概念什么是软约束什么是硬约束。2.1 规则文件的本质是“上下文”AGENTS.md、CLAUDE.md、.cursorrules这些文件本质上都是模型对话上下文的一部分。它们的作用是“告知”是“引导”。当模型生成代码时这些文本会参与注意力计算影响模型的行为。但参与注意力计算意味着什么呢意味着它们和你的主任务描述、代码上下文、历史对话一起竞争模型的注意力。一个模型在生成长代码时很难同时兼顾几十条规则。当任务足够复杂、上下文足够长时规则被稀释几乎是必然的。所以规则文件的最佳定位是表达意图和偏好而不是表达必须满足的验收条件。比如“优先使用组合而不是继承”“错误处理要统一走 ServiceException”这类偏好在规则文件里写是有意义的。但“禁止提交调试代码”“提交信息必须带编号”这类是可以被机器自动验证的约束就不该只靠规则文件来承载。2.2 hooks 的本质是“流程检查点”hooks 原本是 Git 里的概念在特定事件发生前后执行自定义脚本。后来被 AI 编程工具借用来做运行时拦截和反馈。它的工作方式非常简单某个事件发生比如代理要执行一条命令外部程序收到事件内容和参数外部程序给出 verdict放行、拦截、修正代理根据 verdict 决定是继续执行还是改变行动。这个过程不依赖模型的自觉性。模型可能根本不记得你写过“禁止删除 src 目录”但 hook 脚本认得rm -rf src。模型可能不记得提交信息规范但commit-msg脚本会用正则去核对。2.3 软约束与硬约束的对比维度规则文件Hooks / 检查脚本作用方式作为上下文提示模型作为外部命令强制校验故障模式模型“忘记”规则脚本漏配或未触发覆盖范围风格、架构、偏好可机器验证的硬性约束执行主体模型本身Shell、Python、Node 等典型代表AGENTS.md、CLAUDE.mdGit hooks、pre-commit、代理级 hooks对性能影响占用上下文窗口几乎无影响可审计性无法确认是否正确执行有日志、有退出码这个对比引出一个关键判断规则文件处理“应该怎么做”的问题hooks 处理“不能怎么做”的问题。两者不是替代关系而是互补关系。最容易踩的坑是把规则文件当成万能药期望一个文本文件就能约束代理的一切行为。3. 环境准备与前置条件本文示例以 Git 仓库为基础涉及 Shell 脚本、Python 脚本和 JSON 配置。根据你本机环境准备操作系统Linux / macOS 首选Windows 用户建议使用 Git Bash 或 WSL否则 Shell 脚本写法要做调整。Git 版本建议 2.x 以上保证 hooks 功能和退出码处理正常。运行环境示例中的pre-commit配置需要 Python 3如果不想安装 pre-commit也可以用纯 Git hooks 实现。代理工具示例涉及 Claude Code 的 hooks 配置。不同版本的事件名和入参格式可能会有差异本文展示的是通用范式你应以自己安装版本的官方文档为准。检查 Git 是否可用git --version python3 --version node --version不需要安装额外的大型依赖。pre-commit 是一个 Python 工具如果你打算使用它再单独安装pip install pre-commit项目结构建议这样组织your-project/ ├── .git/ # Git 仓库 ├── .ai/ │ └── hooks/ │ └── guard-tool.cjs # 代理级 hook 脚本 ├── scripts/ │ ├── check-debug.cjs # 调试代码检查脚本 │ └── check-commit-msg.sh # 提交信息检查脚本 ├── .pre-commit-config.yaml # pre-commit 配置 └── AGENTS.md # 规则文件软约束这里有一个容易被忽略的细节编写 hooks 脚本时不要默认脚本一定在项目根目录执行。Git hooks 的工作目录是当前仓库根目录但代理工具调用外部脚本时工作目录可能是项目根目录也可能是目录。示例中我会尽量写出相对路径和路径判断。4. 核心流程拆解给你的代理装上强制检查要让 hooks 真正发挥作用需要理解四条设计逻辑。4.1 先写好规则文件作为软约束hooks 负责拦截不代表规则文件可以丢掉。恰恰相反规则文件仍然要写但它应该追求“少而准”。一份有效的规则文件通常包含三类内容项目背景这个项目的架构、技术栈、关键目录代码风格与设计偏好希望模型按什么方式写代码明确禁止项列出模型最容易犯的错误。规则文件不是法律条文不需要覆盖一切。覆盖一切的规则文件模型根本记不住。# 文件路径AGENTS.md ## 项目背景 本仓库是一个 TypeScript 编写的 Node.js 服务采用 pnpm 管理依赖。 核心业务代码在 src/services 目录下不要修改 src/api 之外的模块。 ## 编码要求 - 禁止在源码中新增 console.log / debugger。 - 公共函数必须补充 JSDoc 注释。 - 修改公共模块前先运行 npm test 确认基线。 ## 提交规范 - 提交信息必须以 [TASK-编号] 或 [BUG-编号] 开头。这份文件的价值是让代理“有方向感”但它并不能保证规则被执行。接下来要做的是把其中的硬性条款变成 hooks。4.2 找到关键事件插入检查点一般的执行链路上有四个可以插入检查点的位置事件说明可检查的内容用户提交 prompt代理开始工作前是否包含受限路径、敏感操作代理执行命令前工具调用准备运行时命令是否危险参数是否合法代理结束一次回复后一个大回合完成是否有调试代码、测试是否通过代码提交时准备写入版本库提交信息格式、暂存内容前三个属于代理级 hooks第四个属于 Git 级 hooks。一套完整的防线应该把这四层都覆盖到。不要只依赖其中某一个。4.3 决定拦截策略放行、阻止还是修正hooks 脚本最常见的输出方式是退出码退出码0通过事件可以继续非0失败事件被阻止。代理级 hooks 可能支持更丰富的返回协议例如向 stderr 输出 JSON告诉代理“为什么不放行下一步该怎么改”。这就是修正机制不是简单拦住而是把它引到正确路径上。设计拦截策略时有一个原则只自动拦截可精确判断的内容。正则能判断的、命令能比对的交给 hooks。需要主观判断的、需要业务上下文的交给规则和代码评审。4.4 设计与代理的反馈闭环hooks 不只是“关卡”还是“反馈通道”。当代理的某个行为被拦截时最好让它知道具体原因。以提交信息检查为例拦截脚本应该输出这样格式的信息提交信息必须以 [TASK-编号] 或 [BUG-编号] 开头。 示例[TASK-1234] fix: 修复订单列表中金额精度丢失问题这样代理在下一次提交时会尝试按照反馈修正而不是反复被同一堵墙挡住。反馈信息写得越清晰代理自我修正的成功率越高。5. 完整示例与代码实现下面提供三个可直接复制的示例覆盖 Git 级和代理级两种场景。5.1 示例一用 commit-msg Hook 强制提交信息格式这是最基础、也是效果最立竿见影的强制检查。Git 仓库的.git/hooks/目录下有一个默认的commit-msg模板。我们需要把它替换成可执行脚本。# 文件路径.git/hooks/commit-msg #!/bin/sh # 取提交信息文件路径 commit_msg_file$1 subject$(head -n 1 $commit_msg_file) # 如果提交信息为空直接拒绝 if [ -z $subject ]; then echo 错误提交信息不能为空 2 exit 1 fi # 校验格式[TASK-123] 或 [BUG-456] 或 [FEAT-789] 开头 if ! echo $subject | grep -qE ^(TASK|BUG|FEAT)-[0-9]; then echo 错误提交信息必须以 [TASK-编号] / [BUG-编号] / [FEAT-编号] 开头 2 echo 示例[TASK-1234] fix: 修复订单列表金额精度丢失问题 2 exit 1 fi exit 0保存后给脚本增加执行权限chmod x .git/hooks/commit-msg为什么这个检查能约束编程代理因为大多数编程代理在最终执行git commit时都会走真实的 Git 命令。只要是一个脚本化执行的 Git 提交就会被 commit-msg 钩子拦下来。需要注意git commit --no-verify可以跳过这个钩子。编程代理一般不会这样做但不排除它在某些场景下会尝试。后面第 7 节会专门讲这个问题。5.2 示例二用 pre-commit 阻止调试代码混入单靠提交信息检查还不够。更常见的问题是代理改完代码后遗留了console.log或debugger而开发者根本没有仔细 diff 就直接提交了。用 pre-commit 框架可以把这个检查自动化。首先在项目根目录创建.pre-commit-config.yaml# 文件路径.pre-commit-config.yaml repos: - repo: local hooks: - id: block-debug-code name: Block debug code entry: node scripts/check-debug.cjs language: node types_or: [javascript, ts, tsx, jsx]再创建检查脚本scripts/check-debug.cjs// 文件路径scripts/check-debug.cjs const fs require(fs); const path require(path); const patterns [ /\bconsole\.log\s*\(/, /\bdebugger\s*;?/, ]; const files process.argv.slice(2); let hasError false; for (const file of files) { const content fs.readFileSync(file, utf8); const lines content.split(\n); lines.forEach((line, index) { for (const pattern of patterns) { if (pattern.test(line)) { console.error(禁止提交调试代码: ${file}:${index 1}); console.error( - ${line.trim()}); hasError true; } } }); } if (hasError) { process.exit(1); } process.exit(0);使用前安装 pre-commit 并安装钩子pip install pre-commit pre-commit install安装之后每次执行git commit时pre-commit 都会对暂存区中符合types_or条件的文件运行检查。如果发现有console.log或debugger提交会被拒绝。这个示例体现的意义很明确代理是否记得规则不重要pre-commit 会用脚本替你记得。即使代理在生成代码时“不小心”加了调试语句提交阶段也会被打回。5.3 示例三代理级 Hook在执行命令前拦截危险操作Git 级 hooks 只能在提交时兜底。更前置的防线是在代理“准备执行命令”时拦截。这里以支持 hooks 配置的代理工具为例展示通用范式。在项目的.claude/settings.json或对应配置文件中添加事件监听{ hooks: { PreToolUse: [ { matcher: Bash, hooks: [ { type: command, command: node .ai/hooks/guard-tool.cjs } ] } ] } }对应的guard-tool.cjs脚本负责检查即将执行的命令// 文件路径.ai/hooks/guard-tool.cjs const fs require(fs); // 从 stdin 读取事件 JSON let input ; process.stdin.on(data, chunk (input chunk)); process.stdin.on(end, () { let event; try { event JSON.parse(input); } catch (err) { console.error(无法解析 hook 输入按放行处理); process.exit(0); } // 提取命令内容不同工具字段名可能不同 const command event?.tool_input?.command || event?.command || ; const dangerousPatterns [ /\brm\s-rf\s\/(?!\s)/, /\bgit\spush\s--force/, /\bDROP\sTABLE/i, /\bDELETE\sFROM/i, ]; const matched dangerousPatterns.find(pattern pattern.test(command)); if (matched) { console.error(检测到危险命令已阻止执行: command); try { // 部分代理支持通过 stderr 输出 JSON 来传递阻断原因 console.error(JSON.stringify({ decision: block, reason: 命令匹配危险模式请改用更安全的操作或先向用户确认。 })); } catch (err) { // 忽略 JSON 序列化失败 } process.exit(2); } process.exit(0); });这个示例的重点不是具体字段而是范式代理要执行某个动作时外部脚本先拿到动作内容可以立刻否决。有了这一层代理就算在规则文件里完全没看到“不要删除目录”这句提示也无法绕过实际防护。需要特别提醒代理级 hooks 的配置格式、事件名、参数结构在不同工具和不同版本中并不完全一致。以 Claude Code 为例PreToolUse、PostToolUse 这类事件已经存在较长时间但字段细节可能随版本调整。建议配置前先查看你正在使用的版本的官方 hooks 文档示例代码关注的是“如何判断并返回结果”的通用逻辑。5.4 示例四代理结束回复后自动跑测试前三个示例解决的是“不能做”的问题。还有一类场景是“必须做”要求代理完成修改后必须运行测试。可以在代理的 hooks 配置中注册一个 Stop 事件每次代理准备结束回合时触发测试{ hooks: { Stop: [ { hooks: [ { type: command, command: npm test -- --silent } ] } ] } }这会让代理在“自认为做完”之后被强制造一次验证。如果测试失败代理会收到失败信息从而有机会修正代码而不是把坏代码直接丢给开发者。这个设计的价值是它把“代理主动验证”从可选项变成了必选项。很多时候模型并不是不想遵守规则而是它没有把“运行测试”作为完成任务的必要条件。Stop hook 帮你把这个必要条件注入到流程里。6. 运行结果与效果验证配置完成后需要验证两件事正常提交是否不受影响违规行为是否真的会被拦截。6.1 验证 commit-msg Hook故意提交一条不符合格式的信息git add . git commit -m fix the bug预期输出错误提交信息必须以 [TASK-编号] / [BUG-编号] / [FEAT-编号] 开头 示例[TASK-1234] fix: 修复订单列表金额精度丢失问题再提交一条符合格式的git commit -m [TASK-1234] fix: 修复订单列表金额精度丢失问题此时应该能正常提交。6.2 验证 pre-commit 检查在暂存区放置一个包含console.log的文件echo console.log(debug) src/temp-debug.ts git add src/temp-debug.ts git commit -m [TASK-1234] test debug hook预期输出中会出现禁止提交调试代码: src/temp-debug.ts:1 - console.log(debug)提交失败。开发者常犯的验证错误是“我把脚本放上去了但它到底有没有执行过我并不知道。” 解决办法是故意触发一次违规确认脚本输出的日志真实存在。这也是 hooks 的终极好处——所有检查行为都有日志出了问题可以回溯。6.3 验证代理级 Hook可以手动向guard-tool.cjs喂一段模拟输入观察它是否会拦截危险命令echo {tool_input:{command:rm -rf /}} | node .ai/hooks/guard-tool.cjs echo $?如果脚本正确第二条命令会输出非 0 退出码。这种“离线模拟测试”非常适合在把 hook 接进代理之前先验证逻辑避免调试代理过程被无效脚本干扰。7. 常见问题与排查思路在真正落地 hooks 的过程中会遇到一些出现频率很高的问题。建议对照下表排查。问题现象可能原因排查方式解决方案Hook 脚本没有被执行脚本缺少执行权限检查ls -l .git/hooks/commit-msg执行chmod x增加权限代理用--no-verify绕过 Git hooks代理工具内部绕过 hooks查看代理工具的提交命令日志在代理级 hooks 中拦截“绕过程序”的命令Hook 在 Windows 下报错Shell 脚本不兼容 Windows在 Git Bash 中测试或看错误信息改用 Node/Python 脚本避免 Bash 专有语法pre-commit 没检查到新增文件没有先git add暂存确认文件已在暂存区使用git addpre-commit 只检查暂存内容代理级 hook 放行一切JSON 解析后字段名对不上临时打印event内容按当前工具的文档调整字段提取逻辑Hook 拦截太严格导致正常操作被挡正则写得过于宽泛查看被拦截的命令日志收紧正则加白名单或改更精确的匹配提交仍成功但没经过检查钩子是安装在.git/hooks而不是项目仓库配置检查.git/hooks是否存在脚本使用 pre-commit 安装或团队统一初始化脚本代理收到拦截后仍反复重试同一命令反馈信息不清晰检查 hook 输出的 stderr 内容写明原因和替代方案让代理能自我修正有一个容易被忽略的问题hooks 对代理的约束取决于代理是否真的通过标准流程执行命令。某些代理可能会绕过 Git hooks 直接写文件。所以更稳妥的结构是 Git hooks 加代理级 hooks 同时使用而不是只依赖某一层。8. 最佳实践与工程建议hooks 真正进入工程体系之后你会发现这不是一个“加个脚本”的小事而是一套需要设计和维护的约束系统。下面几条建议来自实际项目中最常见的血泪教训。8.1 让规则文件变短让 hooks 变“硬”规则文件越短模型越可能真正读进去。把可机器检查的内容全部下沉到 hooks。凡是能用正则、AST、命令查询解决的约束都不应该只写在规则文件里。一个实用的分工法则是规则文件负责“为什么、偏好、上下文”hooks 负责“必须、禁止、格式”。8.2 渐进式启用不要一上来全拦截不要在第一天就把所有约束全部打开。先记录再拦截最后再考虑“自动修正”。比如可以先让 hook 把所有违规命令写入日志观察一周确认哪些是真实风险再打开拦截开关。拦截过多、过严代理就无法完成基本任务拦截过少防线名存实亡。比较好的做法是把拦截分级严重违规危险命令、删除目录、强推分支直接阻止中等违规调试代码、测试未通过阻止并给出修改建议轻微违规风格、命名只在日志中记录提示代理修正不阻塞流程。8.3 保证脚本的可移植性hooks 脚本要能被团队成员共享。把脚本放在仓库内而不是放在某个开发者本机的.git/hooks目录里。pre-commit 框架之所以受欢迎正是因为它把 hook 配置和仓库绑定在一起新成员克隆仓库后执行一条pre-commit install就能装上。代理级 hooks 也要尽可能收敛到项目内的.ai/hooks目录配合配置文件一起提交到仓库。8.4 日志和审计要完整hooks 除了拦截违规还有一个隐藏价值审计。每次拦截产生一条日志时间、命令、原因、代理上下文都能被记录。这些数据是理解“代理为什么违规”的最好原料。例如你可以汇总“代理最常触碰的规则”反推规则文件哪里写得不清楚或者哪些操作不适合授权给代理。这个数据驱动的优化路径远比你凭空猜规则要有效。8.5 明确 hooks 的安全边界所有脚本只能做“检查、拒绝、提示”这三件事。不要把业务数据、密钥、用户信息通过 hooks 输出到日志。更不要在 hooks 里写可以执行任意 shell 命令的逻辑否则 hooks 本身会成为新的攻击面。如果代理工具支持更精细的权限控制优先使用最小权限原则。例如只放行指定目录的写操作而不是让代理拥有整个文件系统的能力。9. 总结与后续学习方向回到标题提出的问题编程代理无视你的规则该怎么办答案是——你需要的不是更多规则而是 hooks。规则和 hooks 的关系可以类比成两样东西规则是需求文档hooks 是自动化测试。需求文档写得再好也不能保证代码正确自动化测试虽然不能告诉你架构是否优美但它能保证该通过的条件一定会被验证。两者缺一不可。从实践角度看建议你按下面几步开始改造先梳理你目前最不能容忍的三类代理违规行为把其中可以用脚本验证的变成 Git hooks 或 pre-commit 检查再研究你正在使用的代理工具是否支持事件 hooks在 CI 中补一道同样的检查覆盖所有绕过本地 hooks 的路径观察一周根据日志调整拦截策略。后续值得深入的方向包括pre-commit 的更多官方插件、代理工具的 MCP 协议与权限控制、基于规则评测eval来衡量代理行为质量以及如何把 hooks 推广为团队统一的基础设施。每条方向都比继续“写更多规则”更接近问题的本质。如果你的项目正在被“代理不守规矩”困扰建议先把本文提到的 commit-msg 和 pre-commit 两个示例跑通。它们是最简单、收益最稳定的一步。等这一步稳定了再考虑代理级运行时拦截。最后提醒一句任何 hooks 都可能被绕过。永远不要默认“加了钩子就安全了”关键仓库上的最终保护还是要靠代码评审和 CI 流水线共同完成。