从0打造编码Agent 阅读 + 实验 ≈ 30 分钟 页内实验 ×2
A3 权限与审批:它要执行 rm -rf 怎么办? 第 2 课

两条规则冲突时,听谁的?

规则冲突
同一份规则表交给 Claude Code、OpenCode、CoreCoder 三个真求值器:Claude Code 永远先查 deny,OpenCode 听最后一条命中的,CoreCoder 根本不读规则。拖动规则重排,只有顺序敏感的那一家会翻转。我们自己的项目在真实运行里就撞上过一次。
❓

本课一问:放弃黑名单后,规则交给用户来写。「允许 git *」和「拒绝 git push *」同时命中 git push origin main 时,听谁的?

这一课先不碰:规则没覆盖的调用要不要问人(第 3 课)、规则之外的拦截点 hook(岔路)、让模型来审批(第 4 课)。

从已有出发

上一课的结论:闸门放在 harness 里,按用户写的规则决定放行、拒绝还是问人。我们的项目 miniagent 在修 test_parse_date 的每一次真实运行里,用的都是同一份规则 configs/rules-standard.json:

从已有出发 · 累计数据流 点已学环节看它回答什么
每一轮循环都经过
#工具模式动作
1read_file*放行
2list_dir*放行
3grep*放行
4edit_file*放行
5write_file*放行
6edit_filetest_*拒绝
7write_filetest_*拒绝
8bash*拒绝
9bashpython -m unittest*放行
10bashpython3 -m unittest*放行

它的写法是「通用规则在前、例外写在后面」:先允许编辑一切(#4、#5),再拒绝改测试文件(#6、#7);先拒绝一切 bash(#8),再单独放行跑测试(#9、#10)。没命中任何一条时默认「问人」。多条规则同时命中是常态——python -m unittest … 同时命中 #8 和 #9,一条说拒绝、一条说放行。

先自己设计:按书写顺序,第一条命中的算数

最直觉的规则引擎:从上往下找,第一条命中的规则说了算。它就是 miniagent 的 first_match 语义。下面把真实配置和真实命令交给它,先猜再揭示:

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案按书写顺序,第一条命中的规则说了算
同一份规则,交给不同的求值语义

组件加载中……(需要 JavaScript;所有裁决都在你的浏览器里由移植的原算法现算)

🎯

试三件事:① 先猜 first-match 与 last-match 各给什么,再点「揭示」;② 点「▶ 逐步看求值光标」:first-match 从 #1 往下走、last-match 从 #10 往上走,各自停在哪一行;③ 把 #8 拖到最后一行(或点它的 ↓ 两次,每次挪一格),看「重排记录」里谁改了判。

失败发生在真实运行里:A3 同一份规则改用 first-match(seed 1) 的 trace 记录,第 2 轮的测试命令 executed: false,之后模型只做了 3 次调用(read_file test_dateparse.py、read_file dateparse.py、edit_file dateparse.py),没能再跑一次测试。到目前为止用 first-match 录制的 2 次运行里,测试命令一共被拒 3 次,拒掉它们的规则是 #8。这些运行在 trace 里的每个裁决,本页都用移植的 miniagent 求值器复算过,和录制时写下的完全一致(见实验课的自检)。

改进:三家生产级 harness,三种语义

同一个问题,生产级 harness 给了三种答案。每一种都有明确的理由,也都有代价:

语义谁在用怎么求值为什么这样设计代价
分组 first-match:deny → ask → allowClaude Code先在所有 deny 规则里找,再找 ask,最后找 allow;组内第一条命中定案,「规则写得多具体」不改变顺序拒绝永远优先:一条宽泛的 deny 不会被一条更具体的 allow 意外开口子;组织在托管设置里写的 deny,用户无法覆盖deny 带不了白名单例外。想「拒绝所有 git 但允许 git status」,只能不写 deny、改用 hook(见岔路)
last-match:最后一条命中的说了算OpenCode所有规则拼成一个列表,findLast 从后往前找第一条命中的;都不命中回落 ask规则可以分层拼接:内置默认 → agent 自带 → 用户配置,后来者覆盖前者(agent/agent.ts:119-150 里正是 Permission.merge(defaults, …, user)),写法和 CSS 的层叠一样自然顺序敏感:调整一行的位置,裁决可能静默翻转
只看工具名CoreCoder v0.7.0只读工具直接放行,其余一律问人;「always」按工具记住;没人可问(-p)就拒绝48 行、零配置,没有规则就没有写错的规则表达不了「允许 git status、但 git push 要问」

一个直接的后果:把我们项目的 rules-standard.json 原样翻译成 Claude Code 规则,#8 变成 deny Bash——在 Claude Code 里,不带参数的 deny 会把整个 Bash 工具从模型的上下文里拿掉,#9、#10 放行测试的规则再也救不回来(实验课的矩阵里能看到这一格)。同一份意图,在 Claude Code 里只能改用「allow 测试命令 + 不写 deny」或「allow 整个 Bash + hook 收紧」来表达。

另有两家不在下面的求值器里,只在这里说明:Codex 的 execpolicy 用 Starlark 写 prefix_rule,所有命中规则里取最严格的(execpolicy/src/policy.rs:366 的 .max(),三档 Allow < Prompt < Forbidden 按声明顺序比较,decision.rs:7-16),和顺序无关;它的规则语言是按词前缀匹配、不是通配,且 pin 上的 Rust 源码没法在本机跑出对照 fixture,所以没有移植。learn-claude-code 的 s03 没有 allow 规则,只有写死的三道闸。

权限设计的演进:每一代都被上一代的某个真数字逼出来
  1. Pi 核心、bypassPermissions、CoreCoder --yes
    解决了零打断,模型想跑什么马上跑。
    卡在L1 的 6 条本意危险的构造命令(含 rm -rf /)会全部直接执行。
  2. lcc s01 / s03、CoreCoder bash 正则、Pi permission-gate
    解决了把已知危险的写法挡在执行之前,代码只有几行。
    卡在lcc s03 三道闸(pin 源码实跑):6 条危险命令里漏拦 4 条,5 条日常命令里误拦 2 条(cat sudoku_solver.py 因含子串 sudo 被拒)。
  3. 第一代 每次都问人
    CoreCoder 交互模式、Claude Code Manual、lcc s03 的第三道闸
    解决了判断交给人,规则写不全也不怕。
    卡在我们 26 次真实运行的 139 次工具调用,在 CoreCoder 交互模式下有 74 次要停下来问(53%);Anthropic 自报用户批准了约 93% 的弹窗——问多了,人就不看了。
  4. 第二代 规则匹配:allow / ask / deny 你在这里
    Claude Code(deny → ask → allow)、OpenCode(last-match)、Codex(最严格者胜)
    解决了把常见的放行和拒绝写成规则,只有规则没覆盖的才问人。
    卡在同一份规则换一种求值语义可能换一个答案:同一份 rules-standard.json 改用 first-match,真实运行 pd-firstmatch-s1 里模型的 python -m unittest -v test_dateparse.py 被规则 #8 拒掉。
  5. Claude Code auto、Codex Auto-review(代码名 Guardian)、Hermes smart
    解决了只读直放、项目内编辑免审,其余交给另一个模型判断;弹窗接近清零。
    卡在Anthropic 自报:auto 分类器在真实越权动作上仍有 17% 漏判(n = 52),官方原话「does not guarantee safety」。
  6. 下一章 与 OS 沙箱分层
    Claude Code sandbox、Codex Seatbelt / bubblewrap
    解决了规则管「允许调用什么」,沙箱管「调用之后能碰到什么」。
    卡在L1 的 python3 -c "import shutil; shutil.rmtree(…)" 在四个黑名单里被 4 个全部放行;规则看不见脚本在运行时做什么 → A4。

数字口径:黑名单与「每次都问」由原实现在 pin 源码上实跑(a03-permission-fixtures.json,2026-09-26);真实运行 = 本课程项目 miniagent 用 Qwen/Qwen3-8B-MLX-4bit 本机录制;93% 与 17% 为 Anthropic 官方自报。

核心实验:同一份规则,三个真求值器

同一份规则,交给不同的求值语义

组件加载中……(需要 JavaScript;所有裁决都在你的浏览器里由移植的原算法现算)

🎯

试三件事:① 默认规则集下先猜再揭示:[拒绝 git push *, 允许 git *] 对 git push origin main,Claude Code 与 OpenCode 各给什么;② 点「整表倒序」,看「重排记录」:只有 OpenCode 改判,Claude Code 不动,CoreCoder 从头到尾没看规则;③ 切到「Claude Code 文档里的 aws 例子」,再倒序一次,看下方黄框里「最后一条命中的动作」和「优先级最高的动作」什么时候相同、什么时候不同。

三个求值器的来源各不相同,页面上标清楚:OpenCode 的 evaluate 与 Wildcard.match 是 pin 源码的逐行移植,期望输出由 bun 在 pin 源码上实跑得到;CoreCoder 的 Permission.check 与 _permit 同样对 pin 源码实跑核对;Claude Code 核心闭源,它的求值器是依据官方文档实现的,不是源码移植,只能对官方文档里的例子自检,和真实 CLI 的逐条对照(T12 黄金矩阵)尚未录制。

把 OpenCode 的求值拆开看,它只有一个表达式:

OpenCode 的 evaluate(permission/index.ts:28-38,pin v1.18.32)
rule => && )
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
页面里的 OpenCode 求值器就是这一行的 JS 逐行移植;对 pin 源码的 39 个 evaluate 用例、24 个 Wildcard 用例逐条一致(实验课自检)。

两家什么时候给出相反的答案?可以精确地说:

⚠

常见误解一:「deny → ask → allow、第一条命中的算数,是权限规则的通用语义。」

它只是 Claude Code 的语义。OpenCode 是 last-match、没有动作优先级;Codex execpolicy 取最严格者;CoreCoder 不读规则。把一份 Claude Code 的规则直接翻译成 OpenCode 配置,裁决可能变。

常见误解二:「两种语义对同一份配置一定给出相反结果。」

不一定。只有当「最后一条命中的规则」不是「优先级最高的那条」时才相反;上面的实验里,git status 在两家都是放行。

常见误解三:「Codex 的 Granular 审批里,某一类设成 false 就是遇到它时弹窗问我。」

正好相反。pin 源码的注释:「When a field is true, commands in that category are allowed. When it is false, those requests are automatically rejected instead of shown to the user.」(protocol/src/protocol.rs:932-934)false 是自动拒绝。

🔍 OpenCode 的文档和代码对 .env 说法不一样

OpenCode 文档 permissions.mdx 的「Defaults」一节写 .env 文件默认 deny;而 pin v1.18.32 的代码里,内置默认规则是 read: 「*.env」: 「ask」(packages/opencode/src/agent/agent.ts:130-135)。以代码为准,默认是问人。这类「文档与实现漂移」在读任何 harness 时都要留意:课程引用行为时一律钉 pin、以源码为准。

证据三问

问本课的回答
① 它解决什么问题?多条规则同时命中一次调用时,给出唯一、可预测的裁决
② 为什么三家会不同?求值语义不同:分组优先级、最后命中、只看工具名。同一输入在三种规则下的结果是确定的,相反的条件可以精确写出
③ 各家为什么这样选?Claude Code 选安全优先(deny 不可被覆盖),OpenCode 选分层拼接的便利,CoreCoder 选零配置;都是设计取舍,没有实验证明哪一种更好

一句话

📌

同一组规则,换一种求值语义就可能换一个答案;所以先选语义,再用测试把顺序敏感的用例锁住。

🚦

先停在这里:记住「Claude Code 先查 deny、OpenCode 听最后一条、CoreCoder 不读规则」,以及相反的精确条件。
再深一层:去实验课把真实运行里的全部调用放进四个规则集里跑一遍;或者走岔路:hook 能放宽规则吗。

下一个缺口

规则再好,每个调用都要人点「允许」吗? → 第 3 课:每个调用都问人,人会怎样?

规则覆盖不到的调用,各家都有一个兜底:问人。问多少次、问多了会怎样,是下一课的问题。

你可能会问