1. 凌晨两点那条 exec-approvals 弹窗,到底在问什么
OpenClaw 的 exec-approvals 是一套「命令执行前置审批」机制,简单说就是 AI 助手要跑 shell 命令之前,先弹一个框问你 allow-once、allow-always 还是 deny。它适合谁?适合所有把命令行控制权交给 AI Agent 的开发者——你在用它部署服务、清理磁盘、装依赖、管服务器,那这套审批就是你最后一道手动闸门。这次更新把 allowlist 和 safeBins 的边界讲清楚了,我把它拆成能直接抄的配置和能复现的验证动作。
先说清楚它解决的是什么问题。AI 会执行命令,但它的「理解」和你的「意图」之间永远有条裂缝。你说「帮我清理日志」,它理解成删掉所有带 .log 后缀的文件,然后它用的是find / -name "*.log"。这条命令本身没毛病,但作用域是全盘。exec-approvals 就是填在这条裂缝上的一层补丁:命令落地前,先让你看一眼原始文本,再决定放不放行。
很多人以为它就是个「执行前问你要不要批准」的弹窗。不对。它有两层守卫,而且这两层是独立的。
第一层是 Policy 层,全局策略,决定「哪些命令需要问」。模式有三种:always是所有命令都问,哪怕已经进了 allowlist 也照问;on-miss是只在命令没匹配到 allowlist 时才问;off是彻底躺平,所有命令直接放行。注意off的危险性——只要进了 policy 层,就没有任何阻拦,等于把闸门焊死在打开状态。
第二层是 Allowlist 层,精确到命令的逐条审批。它管的不是「要不要问」,而是「批准了之后能跑多久」。三个选项语义差别很大:allow-once单次有效,精确绑定到这一次命令 ID,执行完立即失效;allow-always永久有效,下次遇到相同命令直接放行;deny永久禁止,进黑名单,下次直接拒绝。
关键问题来了——什么叫「相同命令」?这就是 allow-always 最容易被低估的地方。系统记录的 command ID 是基于命令原始文本生成的。你批准了time npm test,下次 AI 送来time rm -rf /,文本不同,command ID 不同,系统认为这是两条完全不同的命令,会走正常 ask 流程。看起来没问题。
但换个更真实的例子:你批准了curl https://api.example.com/upload -F "file=@docs.pdf",下次 AI 送来curl https://api.example.com/upload -F "file=@secrets.zip"。Command ID 不同,因为文件路径不同,但语义完全一致——都是往同一个 API 端点上传文件。第一次传的是普通文档,第二次传的是敏感数据,allow-always 不会拦你。
这不是 bug,是设计上的权衡。但它的安全含义必须说透:allow-always 不是「授权这类操作」,而是「授权这个 exact 字符串的命令」。理解了这个本质,你就明白为什么很多安全工程师看到 allow-always 会血压升高。
2. safeBins 和 allowlist 到底谁管谁,别再把它们混成一件事
先把结论放前面:safeBins 是「默认信任」,不需要你审批;allowlist 是「我批准了」,代表你主动做了授权决策。这两个机制协同工作,才构成 exec-approvals 的完整权限体系。很多人把它们当成同一套系统的两个名字,这是理解偏差的源头。
safeBins 是一份免审批白名单,由系统或管理员预先定义。它针对的是那些经过验证、风险可控的常见命令,比如ls、cat、pwd。你用这些命令不需要任何审批流程,因为它们本身就是只读或无害的。safeBins 的定位是「这些命令我提前替你信了」,它不记录你的任何决策,是配置层面的静态信任。
allowlist 是你手动添加的审批记录。每一条记录都是你对某条具体命令的授权行为,是动态的、有上下文的。你批准npm test进 allowlist,是因为你验证过这个项目的测试脚本是安全的;你批准某个curl上传命令,是因为你知道那个端点和那个文件是可信的。allowlist 承载的是你的判断,不是系统的预设。
这两者的风险模型完全不同。safeBins 的风险在于「预设是否过宽」——如果管理员把rm塞进 safeBins,那就是灾难。allowlist 的风险在于「授权是否过窄或过宽」——过窄导致效率低,过宽导致 allow-always 把一次信任透支成永久信任。
我实测下来,一个比较稳的组合是:policy 用on-miss,safeBins 只保留真正只读的命令,allowlist 里尽量用allow-once,只有对高频且完全确定的命令才用allow-always。这样既不会每条命令都弹窗,也不会把一次授权变成永久后门。
还有一个容易踩的坑:safeBins 命中的命令不会进 allowlist,也不会留下审批记录。这意味着如果 safeBins 配置过宽,你事后审计时根本看不到这些命令跑过。对于需要合规留痕的场景,safeBins 要收得很紧,宁可多问几次。
allowlist 的命中逻辑也值得说清楚。当 AI 送来一条命令,系统先看 policy:如果是always,直接弹窗,allowlist 不参与;如果是on-miss,先查 allowlist,命中就放行,没命中才弹窗;如果是off,全部放行。所以 allowlist 只在on-miss模式下真正起作用。你如果配了always又指望 allowlist 减少弹窗,那是白配。
理解了这层关系,你就能判断升级后的行为是否可控:先看 policy 模式,再看 safeBins 范围,最后看 allowlist 里有多少条allow-always。这三个数字决定了你的实际安全边界。
3. 可复制的 approvals 配置片段,路径和字段都对齐
下面这份配置我按 OpenClaw 的 approvals 结构写,字段名和层级你可以直接对照自己的配置文件改。核心是三块:policy、safeBins、allowlist。先给一份偏保守的 JSON 片段,适合生产环境或共享服务器。
{ "execApprovals": { "policy": "on-miss", "safeBins": [ "ls", "cat", "pwd", "head", "tail", "wc", "grep" ], "allowlist": [ { "command": "npm test", "decision": "allow-once" }, { "command": "npm run build", "decision": "allow-once" }, { "command": "git status", "decision": "allow-always" } ] } }如果你更习惯 TOML,等价写法是这样:
[execApprovals] policy = "on-miss" [execApprovals.safeBins] bins = ["ls", "cat", "pwd", "head", "tail", "wc", "grep"] [[execApprovals.allowlist]] command = "npm test" decision = "allow-once" [[execApprovals.allowlist]] command = "npm run build" decision = "allow-once" [[execApprovals.allowlist]] command = "git status" decision = "allow-always"几个配置要点必须说清楚。第一,policy我选on-miss而不是always,因为always会让 allowlist 完全失效,每条命令都弹窗,效率低到你想关掉审批。第二,safeBins我只放了只读命令,grep严格说能读任意文件,但在受控目录下风险可控,如果你环境敏感,把grep也拿掉。第三,allowlist里高频只读的git status用allow-always,构建和测试用allow-once,因为构建脚本可能被改,每次确认更稳。
如果你用的是 Claude Code 那套 settings 结构,思路一样,把 approvals 段嵌进去:
{ "permissions": { "execApprovals": { "policy": "on-miss", "safeBins": ["ls", "cat", "pwd"], "allowlist": [ { "command": "npm test", "decision": "allow-once" } ] } } }这里要提醒一句:allow-always 的 command ID 绑定的是原始文本,所以你在 allowlist 里写npm test,AI 实际送来npm test -- --watch时,文本不同,不会命中,还是会弹窗。这不是配置错了,是机制如此。你要么把变体也加进去,要么接受它每次问。
配置改完记得重启 OpenClaw 的 agent 进程,approvals 配置一般在启动时加载,热改不一定生效。我踩过的坑就是改完没重启,以为没生效,折腾了半天。
4. 一次 allowlist 命中与拒绝的验证动作
配置写完不能只看,得跑一次验证,确认命中逻辑和拒绝逻辑都符合预期。下面这套动作你可以直接复现。
第一步,确认 policy 是on-miss,allowlist 里有git status且 decision 是allow-always。然后让 AI 助手执行git status。预期结果:不弹审批框,命令直接执行,输出当前仓库状态。这一步验证的是 allowlist 命中路径。
第二步,让 AI 执行git status --short。预期结果:弹审批框。因为 command ID 基于原始文本,git status --short和git status文本不同,不命中 allowlist,走 ask 流程。这一步验证的是「allow-always 只绑定 exact 字符串」这个关键语义。
第三步,让 AI 执行rm -rf /tmp/test-*。预期结果:弹审批框,且你点 deny 后,这条命令进黑名单,下次再送同样的文本直接拒绝,不再弹窗。这一步验证的是 deny 的持久化行为。
第四步,让 AI 执行ls -la。预期结果:不弹框,因为ls在 safeBins 里。这一步验证 safeBins 的免审批路径。
跑完这四步,你对当前配置的实际行为就有底了。重点看第二步——如果你以为 allow-always 是「授权这类操作」,第二步的结果会纠正你:它只授权那一个字符串。
再补一个边界验证:把 policy 临时改成always,再执行git status。预期结果是弹框,哪怕它在 allowlist 里。这验证了 policy 层优先级高于 allowlist。验证完记得改回on-miss。
如果你在验证时发现 allowlist 没命中,先检查三件事:policy 是不是on-miss;allowlist 里的 command 字符串和 AI 实际送来的文本是否完全一致(包括空格和参数顺序);配置有没有重启加载。这三个查完,基本能定位。
5. 本篇常见报错排查:401、local proxy failed、reading choices、OAuth
接入和验证过程中,报错基本集中在这几类。我按真实遇到的顺序列,每条给排查方向。
401 Unauthorized:最常见的是 Key 没配对或过期。检查你的 Base URL 和 API Key 是否来自同一套环境,别把测试 Key 用到生产端点。如果你用的是 TaoToken 这类统一接入层,确认 Key 是在对应控制台生成的,且没有多余空格。401 也可能是模型 ID 写错,有些端点对模型名大小写敏感。
local proxy failed:本地代理层没起来或端口被占。先确认代理进程在跑,再看端口有没有冲突。如果你在容器里跑,注意容器网络和宿主端口映射。这个报错和审批机制无关,是链路问题,别往 approvals 上查。
reading choices或cannot read property choices of undefined:通常是响应体不是预期的 chat completion 结构,可能端点返回了错误页或空体。先打印原始响应看内容,再确认请求路径是不是/v1/chat/completions这类正确路径。模型 ID 不存在时,有些端点会返回非标准结构,也会触发这个错。
OAuth相关报错:多见于 Claude Code 或 Codex 这类需要登录态的客户端。如果你用 API Key 模式,确认没有残留的 OAuth 配置覆盖了 Key。Codex 的auth.json里如果同时有 OAuth token 和 API Key,可能优先走 OAuth 导致失败。清掉 OAuth 段,只留 Key 配置。
如果你同时用 CC Switch、Cline MCP 或 Codex,记住三件套必须对齐:Base URL、Key、Model ID。任何一件不匹配都会报错。Base URL 指向接入层,Key 是接入层发的,Model ID 是接入层支持的模型名。三者来自同一套配置,别混用。
排查顺序建议:先看 HTTP 状态码,再看响应体原文,最后看配置三件套。大部分问题在第二步就能定位。
6. 把审批边界握在自己手里
回到最开始那个问题:你点了 allow-always,你真的知道自己在允许什么吗?现在你应该清楚了——你允许的是那一个 exact 字符串的命令,不是那一类操作。这个认知差,就是安全边界的关键。
我的建议是:policy 用on-miss,safeBins 只放只读命令,allowlist 里高频确定的用 allow-always,其余用 allow-once。定期审计 allowlist,把不再需要的 allow-always 清掉。这样你既不会被弹窗淹没,也不会把一次信任透支成永久后门。
如果你想把接入层也统一管起来,减少 Key 和端点散落各处的风险,可以走 TaoToken 的 API Keys 页面生成和管理 Key,接入文档里有各客户端的配置示例。需要验证模型连通性时,用模型对话页面直接测一条请求,比在客户端里反复试快得多。长期跑编码和 Agent 任务的话,Coding Plan 更适合高频调用场景。
工具是工具,选择还是你的。审批机制的存在本身,就是在承认一件事:我们不完全信任 AI,但我们在用它。把边界配清楚,比争论站哪边更有用。