09. Permission

Agent 不能直接碰世界:权限六步与「删掉 Bash」

上一篇给写作 Agent 配好了一箱工具,让它能真的读写文件、发布作品。可工具是把双刃剑:能读写文件,就意味着它有能力碰到不该碰的东西。所以这一篇紧接着聊权限,怎么框住每一次工具调用

👆你在这里
👆你在这里

🪧 把它关进授权目录,这就够安全了吗?

让用户授权一个本地目录,把 Agent 关进去,只能读写这个目录里的东西,这听起来是不是很安全?

这个想法不能说错,它是必要的,但还远远不够。举几个它兜不住的例子:这个目录里万一有个 .env 文件、藏着别的密钥呢?Agent 要联网搜资料、要往墨问推送作品,这些「往外」的动作,可不是「关在目录里」就管得住的,它们本来就是要跨出这个目录、甚至跨出这台电脑的

所以「限制目录」只是一道闸门,而一个 Agent 要安全地跟真实世界打交道,需要的是一整套安全系统。这一篇,就来拆一下这套系统,以及在写作这个垂直场景里,是怎么用「减法换安全」的

🧭 基础概念:Agent 不能直接碰世界,中间必须有闸

现在的模型都很聪明,但不可完全信任:它可能被你粘进来的一段外部文字「注入」了坏指令,也可能自己判断失误。所以它作用于真实世界的每一步,读哪个文件、改哪个文件、往哪发东西,中间都必须隔一道闸。这也是 Claude Code Harness Book 专门有一章在讲的:为什么 Agent 不能直接碰世界

我们先看 SDK 本身给了哪些闸,再看我在写作这个垂直场景里是怎么用的

🔩 Claude Code:一套很完整的权限体系,长什么样

先来看下 Claude Code 那套权限与审批体系,它是后面做定制、做减法的基础。Learn Claude Code 的源码解读系列第三篇专门拆了权限机制,大致可分为三类,我用大白话捋一捋:

一是工具的允许 / 拒绝名单。你可以列一份「允许自动放行」的白名单(allowedTools),也可以列一份「一律拒绝」的黑名单(disallowedTools);名单还能细到某个工具的某种用法,比如「Bash 可以用,但 Bash 里的 rm 一律拒」

二是权限模式,几个档位一键切换整体的松紧:default(该问就问)、acceptEdits(工作区内的文件编辑免审批、图个顺手)、plan(只准看、不准动)、以及最松的 bypassPermissions(几乎全放行,非常危险)

三是交互式审批。当一个工具既不在白名单自动放行、又没被黑名单拦死,Claude Code 就当场弹个提示问你,你可以选「这次允许 / 以后都允许 / 拒绝」。你在命令行里用 Claude Code 时那些「要不要允许它执行这条命令」的弹窗,就是这一层

但请记住一个前提:这套体系是为通用编码准备的。所以它默认给的工具很全,包括我上一篇反复念叨的「万能钥匙」Bash(写代码,跑脚本、装依赖是刚需),模式也偏「能用就行」。这套「很全」的默认,如果原封不动地挪到写作场景,就成了能力「过剩」,并带来安全风险

🛠 SDK:一次工具调用,要过一条「六步评估链」

SDK 把权限做成了一条有严格顺序的评估链。模型每想调用一个工具,都要从头走一遍这条链:

工具调用权限链
工具调用权限链

它虽然叫「六步」,展开其实是这么几关,顺序很讲究,越靠前越优先。这里面有三个点,是理解整套权限的关键:

  1. Deny 是硬红线,连「最高权限」都拦不住。 有一种叫 bypassPermissions 的模式能绕过权限模式和 Allow 清单的审批,但它绕不过 deny 规则,也绕不过 Hooks 和 Ask 规则。真正不可逾越的红线,得写在 deny 里
  2. 一个工具一旦进了「Allow 清单」,就走不到最后那步 canUseTool 了。 因为命中 Allow 就直接放行,根本到不了最后那个「弹卡问你」的回调
  3. 最后那个 canUseTool 回调,是「弹窗问用户」的地方。 它会把执行暂停住,等你回答:批准(甚至能顺手改一下工具的入参),或者拒绝(并把拒绝理由回给模型,让它换个法子)。前面几关都没放行的工具,就落到这里,交给用户定夺

记住这三点,也是下面的定制配置的重要参考依据

✂️ SmartWriter:最狠的一刀,先让危险操作无法执行

把这条评估链落到写作 Agent 里,一共涉及到四层配置,但我想先讲最重要的这一层,因为它背后是这一篇我最想传递的一个认识:

最高级的安全,不是「拦得多」,而是「让危险的操作根本不存在」。

拦,是被动的,你得把所有坏情况都想全、都写上规则去拦,漏一个就出事。而「让它压根没有做坏事的能力」,是主动的、一劳永逸的。这句话听着有点玄,但落到我这个产品上,就是一个特别具体、也特别狠的决定。

第一层:把 Bash 整个删了

不是限制它、不是给它加一堆 deny 规则,是从工具集里彻底拿掉,让模型压根不知道有这么个东西。一个写文章的 Agent,要一把能执行任意系统命令的「万能钥匙」干嘛?它并不需要。而 Bash 恰恰是整个 Agent 最大的注入攻击面,也就是「让模型跑脚本、进而碰系统文件」这条最危险的路。把它删掉,这一整类风险就不会发生了,不用绞尽脑汁去拦,因为压根没这道门

删掉 Bash 之后,主写作的工具集收敛为读、写、改、搜(Read/Write/Edit/Glob/Grep),就这几个,再加上第 8 篇那几个受控的专用工具。需要做确定性操作时,我用专门的 custom tool,而不是通用 shell,这也正是上一篇「别让模型用嘴干活」和这一篇「删掉 Bash」两条线的交汇点

🧱 SmartWriter:接着把边界、红线、审批一层层焊上

删完 Bash,剩下三层配置补齐:

第二层,cwd 就是边界。 acceptEdits 这个模式的妙处在于,它的「免审批」只对工作目录之内的编辑生效,一旦模型想动工作目录之外的文件,照样要弹审批。所以我只要把每个写作任务的工作目录设对,Agent 就天然被框在里面了。单篇任务,工作目录就是它自己那个任务目录;合集里的任务,工作目录是合集子目录(这样它能读到合集的画像、记忆和同合集的其他文章,做到上下文传承,但读不到别的合集)

📐 深挖 · 「关进目录」挡得住写,挡不住读

acceptEdits 让「工作目录内的编辑免审批、目录外的编辑要审批」,听起来是一道严丝合缝的墙。但它有个缺口:它管的是「编辑」(Write / Edit),不管「读取」(Read)。 也就是说,模型要是拿一个绝对路径去 Read 目录外的文件(比如去读另一个任务、甚至用户主目录下的东西),acceptEdits 根本不拦

换句话说,「cwd 就是边界」这句话,对「写」严格成立,对「读」并不完全成立。要把「读」也框死在目录里,靠的不是权限模式,而是评估链最前面那一关,PreToolUse hook(我给它起名 path_guard):它在工具真正执行前检查路径,越界的 Read 一律先拦下

权限模式是「便利闸」,不是「隔离墙」。 真正的隔离得靠 hook 亲手焊

第三层,deny 兜底 + hook 白名单。 目录框住了,还得防目录里的敏感文件。我用 deny 规则显式拒掉危险操作,命中 .env.ssh、各种私钥文件,一律拒绝,而且是那种连 bypassPermissions 都绕不过的硬拒绝。更细的路径校验,我放在评估链第一关的 Hooks 里做(具体怎么做,是下一篇的正题,这里先不展开)

第四层,两类动作强制审批。 有两类操作,我认为不应该自动放行,必须让用户亲自点头:

而这个「强制审批」是怎么实现的呢?答案就藏在上一节第 2 个点里:不要把这两类工具放进 Allow 清单。 没进 Allow,它们就走不到「自动放行」,自然一路落到最后的 canUseTool 回调里,也就弹出了审批卡。不需要额外写什么「审批逻辑」,只是利用了评估链的顺序,让该问的工具「自然而然地」落到了问用户的那一关。「靠机制的顺序去实现意图,而不是硬写一堆判断分支」,这种做法和以往的强后端逻辑导向不太一样,也算是这次开发里一个小小的心得

📐 深挖 · 「弹窗问用户」这一步,代码里到底怎么运转

那个 canUseTool 回调,机制比「弹个窗」精细得多。它被触发时,会把整次执行暂停住,把工具名和入参交给用户,然后一直等返回结果。返回的选项有两种:批准,而且可以顺手把入参改一下(比如把模型给的文件路径,收窄到沙箱里再放行);或者拒绝,并附一句理由,这句理由会回给模型,让它据此换个法子,而不是干瞪眼卡死

更妙的是,这个回调可以无限期地挂着等。SDK 本身就支持这个能力,这对桌面 app 理论上太有用了:用户发出指令后,起身去泡了杯茶,好一阵子才回来,这次调用可以就那么悬停着,等他回来、或下次打开时,从持久化的会话里接着往下走,不丢。不过我自己在实际实现中还是加了个超时兜底,避免用户忘了回应就一直卡着

「审批」不只是一道安全闸,它还是人机协作的一个交接点:模型把「我想干这个、参数是这些」摊开,用户来决定放不放行、要不要改。至于这张审批卡长什么样、又怎么跟「模型向你提的澄清问题」共用同一套界面,留到第 14 篇讲 UI 时再细说

🔧 避坑 · 强制审批的工具,绝不能图省事塞进 Allow 清单

这个坑,正是上面那套机制的反面,也是新手容易手滑犯的。在开发调试期间,我为了让联网调研跑得顺,曾直接把 WebSearch 加进了 Allow 清单,省得它老弹窗。结果它一下干出了几十个 web search + web fetch,Token 哇哇的烧。幸好及时反应过来,马上按停

提醒:一旦某个工具放进 Allow,它就在评估链的第 5 关被自动放行了,永远走不到第 6 关那个审批回调

⚖️ 垂直落点:通用求「无所不能」,垂直求「无所不安全」

最后按惯例做一组对照:

⚖️ 取舍现场 · 同一套权限,通用和垂直是两种活法

通用 Agent SmartWriter 为什么
工具集 给全套,追求「无所不能」 收敛到读写搜 + 专用工具,删掉 Bash 写作不需要通用 shell,删了就少一整类风险
安全思路 出问题再加规则去拦 让危险操作「根本不存在」 减法比「拦得全」可靠,不依赖你想周全
危险 / 外发动作 常图顺手自动放行 靠不进 Allow 清单,强制落到审批 花钱、外发不可逆,必须用户点头

通用 Agent 的骄傲,是「我什么都能干」;而做写作这个垂直场景,我的选择恰恰相反,是「我什么危险都干不了」。写作 Agent 的价值在帮你把文章写好,不在能不能跑脚本、能不能碰系统,那些能力对它是纯粹的负担和风险。通过刻意的自我设限,把用不上的能力狠狠砍掉后,剩下的每一样都窄而安全

这一篇为安全而做的减法,和第 5 篇定制 System Prompt、删掉编码人设,有异曲同工之妙,也许是同一种价值观的不同体现:做垂直,得敢于把通用世界里那些「看起来很全」、但自己用不上的东西,一样样砍掉

咳咳,权限这一篇讲完了。细心的你可能已经注意到,在评估链里跳过了排在最最前面的那一关,Hooks,还欠着「路径白名单怎么做」的细节。这一关是特意留到最后讲的,因为它值得单独一篇

它不只是权限链的第一道闸,更是让我们往 Agent 的运转里「插入自己逻辑」的万能钩子,注入、兜底、拦截,很多前面篇章里我说「这个后面会讲」的机关,都藏在它里面。下一篇,我们就来拆解 Hooks。咱们继续聊