一次只改一个测试的修复,diff 却动了四个文件:测试本身、package-lock.json、几个月前就已经在生产跑过的迁移脚本,还有被代理顺手"整理"过的 CI 工作流。单看每一处改动都挺合理,但每一处都可能让你花一天去回滚。

打开网易新闻 查看精彩图片

逐个读 diff 能抓到一部分,而一条在编辑发生前就拒绝的规则能抓到更多。下面这八类路径,各自说明代理改动会引发什么故障,以及怎么在 Claude Code、Codex、Antigravity CLI 和 Gemini CLI 里把它挡住,最后再给没有 deny 语法的代理补一道服务端兜底。

先明确一点:"永不编辑"指的是不用代理自己的文件工具去编辑。锁文件和生成代码照样会变,只是走包管理器或生成器那条路——这条路是故意留着的。

一个前提警告

Claude Code 的文档写明,Read 和 Edit 的 deny 规则覆盖它内置的文件工具、它在 Bash 里能识别的文件命令(如 cat、sed、tee)以及重定向目标,但不覆盖"任意子进程",比如一个自己打开文件的 Python 或 Node 脚本。把这些规则当成对显眼路径的过滤器,操作系统层面的墙是沙箱。

1. 锁文件

按 npm 文档,当锁文件和 package.json 不一致时,npm ci 会报错退出,所以手工打补丁的锁文件会让下一次 CI 跑挂。更糟的情况是它通过了 CI:锁文件里的一条记录可以把某个包解析到另一个 tarball,Liran Tal 演示过这有多容易藏在 diff 里,他反问"谁真的会去审几千行字符改动?"(Snyk)。他的 lockfile-lint 在 CI 里检查来源。

规则:Edit(/package-lock.json),再按你用的工具加上 pnpm-lock.yaml、yarn.lock、uv.lock 或 composer.lock。代理仍然可以运行 npm install,让 npm 自己去写这个文件。

2. 已经跑过的迁移

Flyway 会为每个执行过的迁移存一个 CRC32 校验和,一旦"发现迁移名称、类型或校验和存在差异",校验就会失败。改了已应用的迁移文件,下一次 migrate 就会停下,因为 validateOnMigrate 默认是 true(Flyway 文档)。Rails 按版本号记录跑过的迁移,所以你改完已应用的文件后,它"认为这个迁移已经跑过,于是什么也不做"(Rails 指南)。新库拿到的是改过的版本,生产还是旧版本,而且没有任何东西会报告这个差异。

规则:对 Edit(/db/migrate/**) 用 ask 而不是 deny。代理仍然需要新增迁移,所以每次写入都提示你,你手动拒掉那些碰旧文件的。下面的兜底章节用 CI 检查来强制这一点。

3. CI 工作流

工作流是带着你仓库的密钥运行的。2025 年 9 月,一个标题为"Add Github Actions Security workflow"的提交加进了一个工作流,把密钥发到了攻击者的服务器:来自 817 个仓库的 3,325 个密钥(GitGuardian)。那是攻击者,不是代理。但一个"修 CI"的代理写的是同一个文件,触及范围也一样。

规则:Edit(/.github/workflows/**),如果你把复合 action 放在那里,再加 Edit(/.github/actions/**)。

4. 生成代码

下一次跑生成器会覆盖任何手工改动。Go 用一行匹配 ^// Code generated .* DO NOT EDIT\.$ 的内容标记生成文件(cmd/go)。在 GitHub 上,.gitattributes 里标了 linguist-generated 的路径"默认在 diff 中隐藏"(GitHub 文档),所以那里的手工改动在评审时很容易被划过去。

规则:Edit(/src/gen/**)(换成你真实的输出目录)。代理去改 .proto 或 OpenAPI 源文件,然后跑生成器。生成器是子进程,deny 规则拦不住它,而这正是你要的效果。

5. 第三方依赖副本

go mod vendor 会"重置主模块的 vendor 目录"(cmd/go),所以 vendor/ 里的补丁在下次有人跑它时就消失了。在那之前,go 命令只检查 vendor/modules.txt 是否和 go.mod 里的版本对得上(Go modules 参考),该参考没有描述对 vendor 文件本身的任何检查,所以一份打过补丁的副本可以一路构建、发布,没有任何警告。持久的解法是 fork 加一条 replace 指令。

规则:Edit(/vendor/**),如果你有 third_party/ 也一并加上。

6. CODEOWNERS

CODEOWNERS 决定谁被要求评审什么,配合分支保护还决定谁必须批准。GitHub 从拉取请求的基分支读取它,所以一次编辑不会改变它自己那个 PR 的评审人。可一旦合并,它就改变了此后所有东西由谁评审。一个"清理过期 owner"的代理,可能把你支付目录唯一的必需评审人给删掉。

规则:Edit(/.github/CODEOWNERS),如果你的放在根目录或 docs/ 下,就换成那个位置。

7. 已发布的变更日志条目

release-please 在你合并它的 Release PR 时,根据 Conventional Commits 写 CHANGELOG.md。要修正一条说明,你在已合并 PR 的正文里加一段 BEGIN_COMMIT_OVERRIDE(仅限 squash 合并),release-please 下次运行时会用它,所以它修的是即将发布的版本的说明。代理手工重写已发布的条目,会让变更日志和已经发出去的 tag 对不上。

规则:Edit(/CHANGELOG.md)。如果你保留手写的"Unreleased"段落,改用 ask。

8. 密钥文件

读 .env 会把它的值连同其余上下文一起发给模型。在 Claude Code 里,Read 的 deny 规则同时会挡住同一路径上的 Edit 和 Write,包括在那里新建文件。NotebookEdit 不在覆盖范围内,所以用 notebook 的话再加一条 Edit 规则(文档)。

规则:Read(.env)、Read(.env.*) 和 Read(*.pem)。裸文件名在任意深度都匹配,后面再用 Read(!.env.example) 把你的示例文件挖回来。

Claude Code 的完整配置块

把它提交为 .claude/settings.json。在项目设置里,开头的 / 锚定在会话的工作目录,而在 worktree 会话中 Edit(/src/**) 匹配的是该 worktree 自己的 src/(文档)。规则按 deny、ask、allow 的顺序执行,allow 规则无法从 deny 里挖出例外。

  • deny:Edit(/package-lock.json)、Edit(/.github/workflows/**)、Edit(/src/gen/**)、Edit(/vendor/**)、Edit(/.github/CODEOWNERS)、Edit(/CHANGELOG.md)、Read(.env)、Read(.env.*)、Read(!.env.example)、Read(*.pem)
  • ask:Edit(/db/migrate/**)

规则要写成 Edit(...),不要写 Write(...)。文档说 Claude Code 接受 Write(path) 规则但从不查它,并在启动时给出警告。

Codex、Antigravity CLI 和 Gemini CLI

Codex 在 config.toml 里有权限配置档,标记为 beta。扩展 :workspace 已经让 .git、.agents、.codex 和 .aws 保持只读,设为 read 的路径可读但不可写(文档)。

限制也有:文档只展示 glob 作为 deny 规则、目录作为 read 条目,所以这个配置档把锁文件这类单个文件留给兜底。.env.* 这个 glob 也会把 .env.example 藏起来,而无界的 ** deny 可能需要 glob_scan_max_depth。文档还把配置档描述为沙箱命令的边界、由操作系统沙箱执行,没有说明 Codex 自己的补丁编辑是否在内。信任它之前,先拿一次废弃的编辑试一下。配置档和 sandbox_mode 二选一,不要同时用。

Antigravity CLI 从一个全局文件 ~/.gemini/antigravity-cli/settings.json 读取 action(target) 规则。deny 优先于 ask,ask 优先于 allow,路径相对于工作区根目录(文档)。CLI 文档没有描述按仓库的配置文件,所以这些规则对你打开的每个项目都生效:

  • deny:write_file(.github/)、write_file(vendor/)、write_file(src/gen/)、write_file(package-lock.json)、write_file(CHANGELOG.md)、read_file(.env)

Gemini CLI 在 2026 年 6 月 18 日被 Antigravity CLI 取代,面向未付费档和 Google One 用户(Gemini CLI 文档)。如果你还在跑它,它的策略引擎把 argsPattern 当作正则,去匹配序列化成 JSON 的工具参数。工作区策略文件被禁用,所以这条要放进 ~/.gemini/policies/agent-paths.toml。编辑工具是 write_file 和 replace,两者都接收 file_path(工具):

  • toolName 为 write_file 和 replace,argsPattern 匹配 "file_path":"[^"]*\.github/workflows/,decision 为 deny,priority 为 900

没有 deny 语法的代理,靠服务端兜底

有些代理根本没有路径规则,而任何本地规则都能被机器上的人改掉。服务器不在乎提交是哪个工具写的。

把风险路径(包括 CODEOWNERS 文件本身)收归自己,然后在分支保护里打开"Require review from Code Owners"。没有这个设置,owner 只是被请求评审,批准并不会被强制(GitHub 文档)。

仓库规则集再加两道。"Restrict file paths"会拦下改动匹配路径的提交推送,适合那些谁都不该推的 .env 文件;推送规则集在私有和内部仓库可用(About rulesets)。"Required reviewers"要求特定团队批准对文件模式的改动,用于组织仓库。在 GitHub Enterprise Server 上,一个强制执行的 pre-receive 钩子会拒绝任何脚本以非零退出的推送。

对迁移,在已有文件被改动时让 PR 失败。--diff-filter=MDR 列出被修改、删除和重命名的文件,跳过新增的:

  • 用 git diff --name-only --diff-filter=MDR origin/main...HEAD -- db/migrate 取出改动列表
  • 列表非空就打印"Applied migrations changed:"并 exit 1

漏网的编辑会在哪里现形

我开发 Hermes IDE,看这段时请把这一点算进去。它并排运行 Claude Code、Codex、Antigravity CLI 等代理,每个任务在自己的 hermes/ 分支和 worktree 上。提交进仓库的 .claude/settings.json 是被跟踪的文件,所以它已经在每个任务的 worktree 里了。

规则会漏东西:代理跑的一个脚本仍然能写这些文件中的任何一个。Review Desk(⌘G,Windows 和 Linux 上是 Ctrl+Shift+G)按轮次或按文件展示代理的改动,带风险标记。别指望这份清单里每条路径都有标记:打开按文件视图,自己把这八类路径核一遍。

每个代理轮次都会被保存,所以当某一轮碰了这些路径之一,你可以 diff 那一轮或恢复它,带撤销。一条行内评论会回到写下那行的代理那里。Hermes 免费,以 BSL 1.1 开源,跑在 macOS、Windows 和 Linux 上。

两条规则背后的同一个想法

迁移和变更日志这两条规则,靠的是同一个想法:历史一旦发出去,你只能往后追加,永远不要重写。这个想法贯穿《Event-Driven Architecture Pocket Guide》——事件日志、outbox 表和 saga 都依赖不去编辑已经发生的事。