一次只改一个测试的修复,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 都依赖不去编辑已经发生的事。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.