怎么让 AI 编程 agent 不乱改代码
几乎每个把 agent 放进真实仓库的人,都会在某天下午经历这一幕:它把测试文件当成源码改了,删掉了一整个目录,或者在你想让它重构 A 模块的时候顺手动了 B、C、D。
“跟它说清楚不就行了?“——你把”别乱动”写进提示词,它答应得好好的,十分钟后照样乱来。
这不是它不听话,是你把纪律交给了概率性的机制。提示词是”建议”,模型每次都是现场判断;判断会受上下文污染、会受措辞影响、会疲惫。真正靠谱的约束,必须从”模型自觉”里拿出来,交给确定性的系统。这篇只讲一件事:一个真实仓库里,防乱改代码的最小配置长什么样,按三层防线排。
三层防线,各挡一种乱
| 层 | 挡什么 | 谁在执行 | 特点 |
|---|---|---|---|
| 权限模式 | 它能碰什么:哪些工具、哪些目录 | 工具链 | 粗粒度,一次性设好 |
| 沙盒 | 它真的碰不到:文件系统、网络 | 操作系统级隔离 | 兜底,绕不过 |
| Hooks | 特定的危险动作:rm -rf、强推、删分支 | 确定性脚本 | 细粒度,按动作拦截 |
顺序很重要:先设权限模式把面收窄,再开沙盒兜底,最后用 Hooks 补几个”就算允许也不该做”的动作。指望其中任何一层单独扛,都会漏。
第一层:权限模式,把默认值从”信任”改成”最小”
Claude Code 和 Codex 的权限模型不同名,但思路一致:默认能给多少,而不是默认能碰多少。
- Claude Code:默认的对话里,agent 要改文件会先征求同意;
--dangerously-skip-permissions是反面教材——开了等于把提示词当唯一防线。真正该做的是按项目收紧:只放行它确实要用的工具,把它不该碰的目录明确列为禁区。 - Codex:沙盒 + 审批是两个正交的旋钮(官方说法叫两个旋钮,不是一条滑块)。sandbox 控制”碰得到什么”,approval 控制”做之前要不要问”。把两个旋钮混成一个”从什么都问到什么都别问”的滑条,是默认错误。
判断标准只有一条:这个项目里,它有没有任何一个理由需要 rm -rf? 没有就删掉;有,也应该先想清楚为什么有。
第二层:沙盒,兜底的那张网
沙盒解决的是权限模式漏掉的情况——模型绕过了某个工具限制、或者你误放了某个目录。Claude Code 的沙盒(sandboxing)和 Codex 的 sandbox 都是在操作系统层面限制它能做的系统调用:写不进去的就是写不进去,提示词再花言巧语也没用。
一句话记住这一层:权限模式是”应该”,沙盒是”只能”。 能开沙盒的项目尽量开,它的代价是少数操作(比如需要真正访问网络或系统服务)会被挡,但那些操作本来也不该默认放给 agent。
第三层:Hooks,拦截那些”就算允许也不该做”的动作
前两层管的是范围,Hooks 管的是具体的危险动作。它和前两层的区别很关键:Hooks 是确定性执行的脚本,不是让模型自己判断。
典型的例子:项目里确实需要 agent 跑 git 命令,但你不想让它强推或删分支。给它的 git 权限是真的,可”能跑 git”不代表”能跑 git push --force”。Hooks 在工具调用之前跑一段你写的脚本,脚本说不放行,这次调用就被拦下。
最小可抄的样子(以 Claude Code 的 settings.json 为例,字段名会随版本变化,动手前用官方文档核对):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node ~/.claude/deny-dangerous.mjs \"$CLAUDE_TOOL_INPUT\""
}
]
}
]
}
}
配套的脚本核心就一段:从工具输入里取出命令文本,命中黑名单正则就以非零退出(非零 = 拦截这次调用),并在 stderr 说明原因:
const input = JSON.parse(process.argv[2]);
const cmd = input.tool_input?.command ?? '';
const denied = [
/rm\s+-rf\s+[^ ]*(?:\.git|node_modules|dist|src)/,
/git\s+push\s+--force/,
/git\s+branch\s+-D/,
/curl\s+.*\|\s*(?:bash|sh)/,
];
const hit = denied.find((re) => re.test(cmd));
if (hit) {
console.error(`拦截:命令命中危险规则 ${hit}`);
process.exit(1);
}
三件事值得注意:
- 黑名单只适合”数量很少、语义很明确”的动作。 真要靠正则枚举所有危险命令,你会漏。这就是为什么前两层防线必须先做——Hooks 补的是少量缺口,不是主防线。
- 不要用 Hooks 做需要判断的事。 Hooks 里写”如果这个文件看起来像配置文件就放行”,等于把判断又还给了不确定性。确定性动作才进 Hooks。
- 拦截要能看见。 脚本被触发时要输出原因,不然 agent 会困惑”为什么我跑不了 git”。脚本说人话,agent 才能改走正道。
Codex 的 hooks 机制名称和位置不同,但同一套思路成立;具体以你装的版本文档为准。
五条防误用规则
一:先收权限,再谈 Hooks。 想让 agent 不碰 src/generated/,正确做法是把它划出可写范围,而不是写十条 Hooks 规则去猜它什么时候会碰。
二:别把提示词当防线。 “不要删除任何文件”写在提示词里,模型会遵守到它上下文被污染为止。纪律要落在系统层。
三:黑名单要短。 超过十条的 Hooks 规则说明你在用错误工具解决范围问题,回去看第一层。
四:给 agent 留退路。 拦截后要让它知道为什么、能怎么改。只拦不说,它会反复试、反复被拦,浪费一轮又一轮。
五:验证配置真的生效。 配完先自己手动触发一次被拦的动作,确认脚本真的拦得住——而不是以为配了。检查点:你上一次测试 Hooks 拦截是什么时候?答不上来,说明它可能早就静默失效了。
下一步
这篇给的是”防乱改”的最小框架。三层防线各自怎么调得更细、Hooks 怎么写才不误伤,教程站有完整实现:
- 想系统理解权限怎么分:教程站《Claude Code 上手指南》第 4 章:权限模式
- 想自己写 Hooks 和技能:教程站《给 AI 写规矩》第 7、8 章:确定性钩子与生命周期
- 用 Codex 的话,看《把 Codex 用成队友》第 4 章:沙盒与审批
提醒:权限字段名、Hooks 配置格式与默认值都随版本变化,动手前以你当前版本的官方文档为准。上面的正则只是示例,别指望它能覆盖所有危险命令。