怎么让 AI 编程 agent 不乱改代码

AI 编程权限安全

几乎每个把 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);
}

三件事值得注意:

  1. 黑名单只适合”数量很少、语义很明确”的动作。 真要靠正则枚举所有危险命令,你会漏。这就是为什么前两层防线必须先做——Hooks 补的是少量缺口,不是主防线。
  2. 不要用 Hooks 做需要判断的事。 Hooks 里写”如果这个文件看起来像配置文件就放行”,等于把判断又还给了不确定性。确定性动作才进 Hooks。
  3. 拦截要能看见。 脚本被触发时要输出原因,不然 agent 会困惑”为什么我跑不了 git”。脚本说人话,agent 才能改走正道。

Codex 的 hooks 机制名称和位置不同,但同一套思路成立;具体以你装的版本文档为准。

五条防误用规则

一:先收权限,再谈 Hooks。 想让 agent 不碰 src/generated/,正确做法是把它划出可写范围,而不是写十条 Hooks 规则去猜它什么时候会碰。

二:别把提示词当防线。 “不要删除任何文件”写在提示词里,模型会遵守到它上下文被污染为止。纪律要落在系统层。

三:黑名单要短。 超过十条的 Hooks 规则说明你在用错误工具解决范围问题,回去看第一层。

四:给 agent 留退路。 拦截后要让它知道为什么、能怎么改。只拦不说,它会反复试、反复被拦,浪费一轮又一轮。

五:验证配置真的生效。 配完先自己手动触发一次被拦的动作,确认脚本真的拦得住——而不是以为配了。检查点:你上一次测试 Hooks 拦截是什么时候?答不上来,说明它可能早就静默失效了。

下一步

这篇给的是”防乱改”的最小框架。三层防线各自怎么调得更细、Hooks 怎么写才不误伤,教程站有完整实现:

提醒:权限字段名、Hooks 配置格式与默认值都随版本变化,动手前以你当前版本的官方文档为准。上面的正则只是示例,别指望它能覆盖所有危险命令。