权限模式与规则

四种权限模式在各入口的默认值与真实语义,PermissionService 的判定顺序,规则的结构与匹配算法,审批请求怎样经 broker 交给终端或桌面并与钩子竞速,“始终允许”与 Full access 各存在哪一层,以及 Plan 与 yolo 的例外。

作者 David更新于 21 篇(共 47 篇)

模型每发起一次工具调用,执行器都要先回答一个问题:放行、拒绝,还是停下来问人。ZCode 把这件事拆成三块。apps/zcode-cli/packages/core/src/permission/service.ts 里的 PermissionService 是同步的纯判定,只给出 allowaskdeny 三种结论(service.ts:59);apps/zcode-cli/packages/core/src/tool/executor/permission-*.ts 把结论落地:加载项目规则、叠加钩子的意见、把询问交给 broker、保存“始终允许”;broker 则由宿主提供,终端 TUI、无头 -p 和桌面与 Web 背后的 ZCode Protocol 服务端各有一份。

权限在工具调用流水线里的位置(输入校验与 PreToolUse 钩子之后、handler 之前)见执行器:调度、审批、超时与结果,Bash 命令怎样被判成“只读”见 Bash:解析、只读判定与后台任务

四种模式与各入口的默认值

CollaborationMode 有五个值(apps/zcode-cli/packages/contracts/src/interfaces/session.port.ts:32),但运行时真正保存的执行状态只有两个字段:权限档位 modebuildedityoloauto)和独立的 planEnabled 开关,plan 只在读取旧格式的边界上被接受(packages/shared/src/execution-state.ts:3)。传入 mode: "plan" 时档位保持原值,只把 planEnabled 置为 true(src/execution-state.ts:21),AgentRuntime 构造时就按这条规则归一配置(apps/zcode-cli/packages/core/src/runtime/agent-runtime.ts:237)。所以 Plan 其实是叠在某个档位上的只读约束,TUI 只是在 planEnabled 为真时把它显示成 Plan(apps/zcode-cli/packages/cli/src/tui-command-state.ts:48);Plan 与 Goal 也不能同时开启(apps/zcode-cli/packages/core/src/runtime/execution-state.ts:58)。

模式TUI 里的说明代码里的语义
buildAsk before each file changes.只读工具放行,有副作用的询问
editEdit selected files or relevant workspace files automatically.在 build 之上,工作区文件编辑直接放行
planInspect the code and present a plan before editing.当前档位加 planEnabled:只读、非破坏性的放行,其余拒绝
yoloEdit and run commands with fewer confirmations.普通工具一律放行,交互类与 alwaysAsk 工具除外
auto不在 TUI 里出现预留未实现,普通工具一律拒绝(service.ts:140

说明文字出自 apps/zcode-cli/packages/tui/src/app-mode-command.ts:10。TUI 里 Shift+Tab 按 plan、build、edit、yolo 的顺序轮换(apps/zcode-cli/packages/tui/src/app-mode.ts:5apps/zcode-cli/packages/tui/src/app-keyboard-helpers.ts:73),/mode 可以直接选。命令行的 --mode 只接受这四个值,大小写不敏感,传 auto 会报错退出(apps/zcode-cli/packages/cli/src/run.ts:133)。根目录 NOTICE 对默认值的说明是(NOTICE.md:11):

共享运行配置默认采用 build 权限模式;独立 CLI 通过 --prompt 执行非交互任务时,未指定 --mode 会采用 yolo

代码与之一致,但入口之间的差别更细:

入口没有显式指定模式时出处
zcode -p "..."固定补 yolorun.ts:42run.ts:494
zcode --target "..."不补默认,走下面的回落链run.ts:510
zcodezcode tui回落链apps/zcode-cli/packages/cli/src/tui-prompt-handler.ts:160
桌面、Web(经 app-serversession/create 参数里的 mode,没有再走回落链apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server-operations.ts:3328

回落链是:显式模式、本项目上次选过的模式、配置项 permission.mode(默认 buildapps/zcode-cli/packages/contracts/src/config/index.ts:295),见 apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:241apps/zcode-cli/packages/bootstrap/src/app/runtime-config.ts:123。“上次选过的模式”是每次切换模式时写下的项目级偏好(apps/zcode-cli/packages/bootstrap/src/app/session-facade.ts:428)。恢复旧会话时,会话自己保存的执行状态又优先于项目偏好(apps/zcode-cli/packages/core/src/runtime/methods/resume.ts:213)。

配置文件的 permission 段有五个键(apps/zcode-cli/packages/adapters/src/config/schema.ts:13):mode 就是上面说的默认档位;allowedToolsdisallowedTools 按精确工具名比较;autoApproveHighRisk 只跳过“高风险要问”那一格,Bash 这类声明了 needsApproval 的工具随后仍会在“有副作用”一格被问到(service.ts:474service.ts:497);allowMediumRiskInAuto 会传进 PermissionServicecreate-app.ts:346),但判定逻辑里没有任何地方读它。

各模式下放行、询问还是拒绝

按各工具声明的 readOnlydestructivesideEffectScoperiskLevelneedsApproval 走一遍判定,常用工具的结论如下:

工具buildeditplanyolo
只读:ReadGlobGrepWebSearchTodoWriteAgentSkill放行放行放行放行
写:WriteEdit询问放行拒绝放行
Bash,命令被判为只读放行放行放行放行
Bash,其他命令询问询问拒绝放行
网络:WebFetch询问,预批准域名放行同 build放行放行
MCP 工具询问询问未声明 destructiveHint 的放行放行
交互:AskUserQuestion询问询问询问询问
ExitPlanMode拒绝拒绝询问(即计划审批)拒绝
CreateWorkflowAmendWorkflowSaveWorkflow询问询问询问询问

几处需要解释:WebFetch 声明为只读但 needsApproval 为真(apps/zcode-cli/packages/core/src/tool/handlers/webfetch.ts:201),build 下要问,Plan 只看只读与否,所以反而放行;Bash 的静态声明是 highsystemapps/zcode-cli/packages/core/src/tool/handlers/bash.ts:456),只有只读判定通过时才在运行时改成低风险只读(bash.ts:77);MCP 工具一律 needsApproval: true,只读与破坏性取自服务端给的注解(apps/zcode-cli/packages/core/src/mcp/index.ts:108)。EnterPlanMode 在任何模式下都直接放行(apps/zcode-cli/packages/core/src/permission/plan-mode-policy.ts:23)。

PermissionService 的判定顺序

checkPermissionservice.ts:97)从上到下,先命中者定案:

图表加载中…

中间最关键的一段是 service.ts:130

    // 声明 alwaysAsk 的工具必须经过用户确认,不能被权限模式的放行分支绕过。
    if (capability.alwaysAsk) {
      return this.checkAlwaysAsk(context, capability, projectRules, rulePolicy);
    }

    const planEnabled = context.planEnabled ?? context.mode === "plan";
    if (context.mode === "yolo" && !planEnabled) {
      return this.allow(context, capability, "mode.yolo", "Yolo mode bypasses permission prompts");
    }

    if (context.mode === "auto") {
      return this.deny(
        context,
        capability,
        "mode.auto.unimplemented",
        "Auto mode is reserved but not implemented yet",
      );
    }

    if (this.config.disallowedTools.has(context.toolName)) {
      return this.deny(
        context,
        capability,
        "rule.disallowedTools",
        `Tool ${context.toolName} is explicitly disallowed`,
      );
    }

yolo 排在禁用名单和项目规则之前,这是有意为之:checkAlwaysAsk 的注释写明它只为 alwaysAsk 工具补一遍硬阻断,不改其他工具的既有优先级,“尤其 yolo 目前先于 disallowedTools 放行这一点”(service.ts:331)。build 规则(service.ts:452)依次是:只读、非破坏且不需审批的放行;critical 询问;high 在没开 autoApproveHighRisk 时询问;作用域为 session 的低风险操作放行;需要审批、有破坏性或副作用作用域不是 none 的询问;剩下的放行。edit 只多一条:权限名是 edit 且作用在工作区的直接放行(service.ts:518)。

判定结果还要经过两道叠加(apps/zcode-cli/packages/core/src/tool/executor/permission-flow.ts:92)。一是 PreToolUse 钩子:钩子说 allow 能把询问变成放行,但抹不掉 alwaysAsk 的询问;钩子说 ask 能把放行变成询问;拒绝则原样保留(apps/zcode-cli/packages/core/src/tool/executor/hook-flow.ts:195)。二是记忆文件:WriteEdit 写进记忆目录的 .md 文件一律放行,连 Plan 的拒绝也会被改写,只有其他拒绝、alwaysAsk、项目 ask 规则和钩子的 ask 保留(apps/zcode-cli/packages/core/src/tool/executor/memory-file-permission.ts:74),记忆本身见项目记忆

规则的结构与匹配

规则不是 Bash(git status:*) 这样的字符串,而是两个字段的结构:toolName 加可选的 ruleContentapps/zcode-cli/packages/contracts/src/interfaces/permission.port.ts:19),一套规则集按 allowdenyask 分组(permission.port.ts:24)。Claude Code 把工具名和内容写进同一个 Tool(specifier) 字符串(见配置文件),ZCode 则分开存,一条前缀规则写作 toolNameBashruleContentgit status:*。括号写法只出现在工具禁用名单里(命令行的 --disallowedTools 与协议会话参数),而且括号里的内容会被丢掉,后面细说。

匹配分三步(service.ts:233):

  1. 工具名:精确相等;Edit 规则同时覆盖 Writeservice.ts:264);保留键 zcode:permission-capability:official_cuapackages/shared/src/zcode-protocol-legacy-types.ts:45)只匹配宿主验证过的官方 Computer Use 工具,同名第三方 MCP 冒充不了(service.ts:274)。
  2. 取主体:没有 ruleContent 的规则匹配整个工具;否则从输入里取第一个字符串字段,顺序是 commandurlfile_pathpathpatternpatch_textservice.ts:292)。WebFetch 例外,主体是 domain: 加小写主机名(apps/zcode-cli/packages/core/src/permission/rule-matching.ts:19)。
  3. 比内容service.ts:307):
  private matchesRuleContent(subject: string, ruleContent: string): boolean {
    if (ruleContent.endsWith(":*")) {
      const prefix = ruleContent.slice(0, -2);
      return (
        subject === prefix || subject.startsWith(`${prefix} `) || subject.startsWith(`${prefix}\t`)
      );
    }

    if (ruleContent.includes("*")) {
      return wildcardToRegExp(ruleContent).test(subject);
    }

    return subject === ruleContent;
  }

:* 结尾是“前缀加词边界”,git status:* 能匹配 git statusgit status -s,匹配不了 git statusx;其余的 * 转成首尾锚定的通配正则(rule-matching.ts:1);都没有就逐字相等。

Bash 不走这里,而是由工具自带的规则策略判定(apps/zcode-cli/packages/core/src/tool/handlers/bash-command-permission-policy.ts:94):整条命令逐字相等的规则总能命中;命令能被安全解析(没有重定向、没有动态词)时,按 &&| 等拆出的子命令逐个比,deny 与 ask 规则命中任意一个子命令即成立,allow 规则则要求每个非只读子命令都被覆盖,只读子命令免检;解析不安全时只认逐字相等(apps/zcode-cli/packages/core/src/tool/handlers/bash-command-rule-evaluator.ts:13)。拆分与只读判定的细节归 Bash

项目规则的位次也要记住:deny、ask 在 Plan 分支之前,allow 在其后,所以“始终允许”解不开 Plan 的只读约束;yolo 在它们之前,所以 yolo 下项目规则都不看。

审批:从 broker 到终端与桌面

判定为 ask 后,执行器先调用工具自带的 prepareApproval(只有工作流一类工具声明了),它可以因为“脚本根本编不过”之类的理由不弹窗、直接交给 handler 回诊断(apps/zcode-cli/packages/core/src/tool/handlers/create-workflow.ts:13),但生成预览出错时一律照问(apps/zcode-cli/packages/core/src/tool/executor/approval-gate.ts:36)。接着生成 perm_ 加 UUID 的请求 ID(permission-flow.ts:160),发出 PermissionRequested 事件,事件里带着建议规则,以及会话存储是否支持 Full access(apps/zcode-cli/packages/core/src/tool/executor/events.ts:136)。然后,PermissionRequest 钩子链与 broker 并发竞速:

图表加载中…

注释交代了为什么要竞速:过去钩子链先串行跑完才启动 broker,同步阻塞的钩子挂着时确认窗已经显示,但点击没有归宿,“确认窗永久死亡”(permission-flow.ts:182)。竞速本身(apps/zcode-cli/packages/core/src/tool/executor/permission-responder-race.ts:59):

      // broker 先启动:requestPermission 内同步注册应答 deferred,保证确认窗一可见
      // 用户点击就有归宿,不给 hook 留任何独占窗口期。
      input
        .requestBroker(brokerController.signal, () => {
          if (settled || brokerController.signal.aborted) return false;
          brokerClaimed = true;
          hookController.abort();
          return true;
        })
        .then(
          (result) => settle(() => resolve({ result, source: "broker" }), hookController),
      // ...
      input.runHooks(hookController.signal).then(
        (decision) => {
          // 无决定 = 退赛:不迁移竞速状态,broker 继续单边等待。
          if (decision === undefined || brokerClaimed) return;
          settle(() => resolve({ result: decision, source: "hook" }), brokerController);
        },

传给 broker 的 claimResponse 回调用于带副作用的应答:Full access 要先“认领”胜者、中止钩子,再去提交事务。钩子链出错只算退赛,不替用户做拒绝决定(permission-flow.ts:185)。等待没有默认超时:permissionTimeoutMs 不配就不传(apps/zcode-cli/packages/core/src/runtime/types.ts:132)。

broker 有几种实现:

宿主broker行为
没有配置应答方DenyPermissionBroker运行时的缺省值(agent-runtime.ts:249),一律拒绝,理由 No permission client configuredapps/zcode-cli/packages/core/src/permission/broker.ts:24
TUI进程内转发转给审批面板,面板不在时拒绝(tui-prompt-handler.ts:78
-p 无头createHeadlessPermissionBroker只放行 CreateWorkflowAmendWorkflow,其余委托给拒绝 broker(apps/zcode-cli/packages/cli/src/headless-workflow.ts:31
桌面、Web协议 brokerAskUserQuestion 转问卷,ExitPlanMode 转计划审批,其余转审批卡片(apps/zcode-cli/packages/bootstrap/src/zcode-protocol/interaction-broker.ts:43

协议侧还有第二层竞速:同一请求同时以旧协议的反向请求 interaction/requestPermission 和 V4 的待处理交互挂出,谁先应答算谁(apps/zcode-cli/packages/bootstrap/src/zcode-protocol/interaction-response-race.ts:20);等待期间按同一个请求 ID 重发,间隔从 1 秒起每次翻倍、封顶 10 秒,让从快照恢复的界面重新登记(interaction-broker.ts:41apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server.ts:121zcode-protocol/server.ts:882);多个客户端同时点,先到先得,晚到的应答按幂等成功收口(apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/commands/handlers/interaction-background.ts:41);认不出的 optionId 一律按拒绝处理(interaction-broker.ts:191)。

审批选项由 buildProtocolPermissionOptions 生成(apps/zcode-cli/packages/bootstrap/src/permission-options.ts:37):Allow once、Always allow in this project、Deny;工具声明 askOptions.allowAlways: "session" 时第二项换成 Always allow in this session,声明 false 时去掉第二项。在桌面与 Web 上拒绝时,模型收到的工具结果写明工具没有执行、请停下等待指示,填写的反馈接在后面(permission-options.ts:8permission-options.ts:11)。TUI 的面板只有 Allow once、Always allow in this project、Deny 三项,Esc 等于拒绝(apps/zcode-cli/packages/tui/src/app-approval.ts:49),拒绝理由只是一句 Denied in TUIapp-approval.ts:130);它不处理会话选项,也没有 Full access;CreateWorkflowAmendWorkflow 的确认在 TUI 里被自动放行,注释称之为“记录在案的 CLI 例外”(apps/zcode-cli/packages/tui/src/app-permission.ts:16)。

“始终允许”存在哪一层

应答结果里的字段存到哪里何时失效
Always allow in this projectpermissionUpdates会话库 local_setting 表,scope 为 project、namespace 为 permission、key 为 ruleset仓库里没有找到查看或删除它的命令与界面
Always allow in this sessionsessionPermissionUpdatesPermissionService 实例的内存重启、冷恢复、/new
PermissionRequest 钩子返回的规则permissionUpdates同项目规则同上
Full access独立命令会话的执行状态与授权回执把模式切回去

项目规则的读写在 apps/zcode-cli/packages/core/src/tool/executor/permission-rules-persistence.ts:14,合并时按工具名加内容去重(apps/zcode-cli/packages/core/src/tool/executor/permission-rules.ts:32),落进 local_setting 表(apps/zcode-cli/packages/adapters/src/storage/session-store/repositories/local-settings.ts:27),读取时旧的 permission 表仍作兜底(local-settings.ts:21)。会话库默认在 ~/.zcode/cli/db/db.sqliteapps/zcode-cli/packages/contracts/src/config/index.ts:303)。“项目”指会话的 projectID:工作目录路径转小写,连续的非 [a-z0-9._-] 字符折成一个 -,去掉首尾的 -,只取前 80 个字符(apps/zcode-cli/packages/bootstrap/src/app/paths.ts:29),所以从代码看,前 80 个字符相同的两个深层目录会共用一套规则。

会话规则只服务 alwaysAsk 那道门,普通工具的判定不看它(service.ts:84service.ts:365);会话选项授予的是整个工具、不带内容,注释给的理由是脚本每次都不同,授权对象只能是工具本身(permission-options.ts:105)。

权限建议怎样生成

确认窗里“始终允许”要保存的规则,就是 PermissionRequested 携带的建议。非 Bash 工具由 buildDefaultPermissionUpdates 生成:工具名,加输入里第一个非空的 commandurlfile_pathpathpattern 作为内容(apps/zcode-cli/packages/core/src/tool/executor/permission-suggestions.ts:6),官方 Computer Use 工具则授予上面那个保留键。Bash 的建议来自规则策略(bash-command-permission-policy.ts:135):只为非只读的子命令生成规则,每个子命令求一个稳定前缀再加 :*,例如 npm run build 得到 npm run build:*python -m pytest 得到 python -m pytest:*make test 得到 make test:*;前缀求不出、根命令属于 rmchmodshdd 等高风险名单(bash-command-permission-policy.ts:19)、命令解析不安全或需要超过 5 条规则时,退回逐字保存整条命令,前缀怎样求见 Bash。TUI 与桌面会把其中的 Bash 前缀列在选项旁边,最多 5 条(app-approval.ts:91)。

从代码看,WebFetch 的默认建议把整条 URL 写进 ruleContent,而判定时拿来比的是 domain: 加主机名,两者对不上,这条“始终允许”不会生效;想按域名放行,得有人写 domain:example.com 形式的规则,目前只有 PermissionRequest 钩子能写入。

审批之后输入又被改了

PreToolUse 钩子改写输入发生在判定之前,权限看到的已经是改后的输入。麻烦的是 PermissionRequest 钩子:它可以在应答时带 updatedInput,边批准边改参数,比如把写入目标换个路径。这时执行器先把新输入归一化、按 schema 校验,再用 recheckPermissionHookModifiedInput 以改后的输入重跑一遍 checkPermission 与记忆文件规则(apps/zcode-cli/packages/core/src/tool/executor/permission-input-recheck.ts:24)。结果分三种(permission-input-recheck.ts:65):被拒绝,整次调用拒绝;是普通的询问或放行,钩子本身就算批准人,沿用它的决定;只有撞上项目 ask 规则或指向记忆文件时,才用同一个请求 ID 再问一次用户(permission-input-recheck.ts:88)。用户自己在确认窗里改输入(modify)则只重新归一化和校验,不再重判(permission-flow.ts:415)。

Full access

桌面与 Web 的审批卡片上还有第四个按钮 Full access(apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/product-projection.ts:3146)。它出现需要三个条件:会话存储实现了 commitPermissionFullAccess,请求不是子 Agent 转上来的,工具也没有声明选项策略(interaction-broker.ts:102)。按下后 grantPermissionFullAccess 把本会话切到 yoloplanEnabled 不变,同时把队列里已受理、还没开始的输入也改成 yolo(apps/zcode-cli/packages/core/src/runtime/permission-full-access.ts:18),然后当前请求按“允许一次”收口(apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/interaction-registry.ts:170)。这些写入在一个 begin immediate 的 SQLite 事务里完成,同一张回执已存在就直接提交返回(apps/zcode-cli/packages/adapters/src/storage/session-store/repositories/permission-full-access.ts:17),改写队列的部分在 repositories/permission-full-access.ts:34

    for (const id of input.queueItemIds) {
      const row = read.get(id, input.sessionID);
      if (!row || typeof row.payload !== "string")
        throw new Error(`Pending input unavailable: ${id}`);
      const payload = JSON.parse(row.payload) as Record<string, unknown>;
      for (const key of ["intent", "conversationInputIntent"]) {
        const intent = payload[key];
        if (intent && typeof intent === "object" && !Array.isArray(intent)) {
          payload[key] = { ...intent, mode: "yolo" };
        }
      }
      write.run(JSON.stringify(payload), Date.now(), id, input.sessionID);
    }
    saveSessionEntry(db, input.execution);
    saveSessionEntry(db, input.receipt);
    db.exec("commit");

回执 ID 由会话 ID 加交互 ID 拼成(apps/zcode-cli/packages/core/src/runtime/permission-full-access.ts:37),类型是 runtime/permission_full_accessapps/zcode-cli/packages/contracts/src/interfaces/permission-full-access.ts:3),重试时按回执里固定的队列范围重放,不会顺手把后来入队的输入也提权(apps/zcode-cli/packages/core/src/runtime/permission-full-access.ts:17)。事务已提交而事件没来得及发布时,恢复动作登记在 unpublishedPermissionGrants 里,下一次授权或模式切换会先补发(apps/zcode-cli/packages/core/src/runtime/permission-grant-recovery.ts:3apps/zcode-cli/packages/core/src/runtime/execution-state.ts:50)。它只改这一个会话,不写项目偏好;恢复会话时读回执行状态,yolo 仍在。代码里没有单独的“撤销完全访问”,把模式切回去就是关闭。老版本客户端若原样回传这个选项的 response,拿到的是一条拒绝(product-projection.ts:3152)。

Plan 的只读约束与草稿例外

Plan 分支(service.ts:404)只放三类:只读且非破坏的工具;非破坏的 MCP 工具;声明 allowedInPlanMode、作用域为会话、非破坏且不需审批的控制动作,目前只有 RespondToCoordinatorapps/zcode-cli/packages/core/src/tool/handlers/respond-to-coordinator.ts:65)。其余一律以 Plan mode only allows read-only, non-destructive tools 拒绝。ExitPlanMode 声明需要用户交互(apps/zcode-cli/packages/core/src/tool/handlers/plan-mode.ts:155),在 Plan 里总是询问,协议 broker 把它转成计划审批问卷,这部分交互见 Todo、提问与 Plan 模式

从代码看,Plan 对 MCP 工具只看 destructiveHint 注解。浏览器插件启用时由宿主注册的 mcp__node_repl__js 能在本机执行任意 Node 代码,mcp/index.ts 为它设了 high 风险与 system 作用域,却没有设破坏性,宿主工具列表里也没有注解(apps/zcode-cli/packages/node-repl-host/src/server.ts:76),因此它在 Plan 下会被放行而不询问。内置版本的 js 工具权限名是 node_repl,不受此影响。

两个“写入例外”容易混淆。记忆文件的放行连 Plan 也覆盖,前面已经讲过。工作流草稿的免确认则不覆盖 Plan:WriteEditfile_path 落在 <工作目录>/.zcode/workflow-drafts/ 之内时放行(apps/zcode-cli/packages/core/src/permission/workflow-draft-path.ts:73,目录常量见 apps/zcode-cli/packages/contracts/src/tools/saved-workflow.ts:25),但它排在 Plan 分支之后,注释说 Plan 必须继续拦下一切写入,草稿也是写入(service.ts:198)。这条判定只做路径字符串运算,不碰文件系统,拿不到工作目录就不放行;理由是那个目录由工具自己写、自带忽略全部内容的 .gitignore,而让脚本真正跑起来另有 CreateWorkflow 的确认门(workflow-draft-path.ts:52)。

yolo 下仍要确认的

  • 需要用户交互的工具:AskUserQuestionExitPlanModeservice.ts:112)。
  • 声明 alwaysAsk 的工具:CreateWorkflowAmendWorkflowSaveWorkflowapps/zcode-cli/packages/core/src/tool/handlers/create-workflow.ts:352apps/zcode-cli/packages/core/src/tool/handlers/save-workflow.ts:286)。前两个可以选本会话免确认,修订本会话自己发起、且不是用户亲手停下的 run 也免确认(service.ts:378);SaveWorkflow 不给“始终允许”。
  • 开着 Plan 的 yolo:yolo && !planEnabled 不成立,照 Plan 规则走。
  • 钩子:PreToolUse 钩子的 deny 与 ask 在 yolo 下照样生效。

反过来,yolo 会跳过项目的 deny、ask 规则和配置文件里的 permission.disallowedTools,所以想在 yolo 下禁掉某个工具,要用 --disallowedTools 把它从工具面上拿掉。子 Agent 中,general-purpose 与自定义 Agent 共用父会话的 PermissionService,Explore 另起一份(apps/zcode-cli/packages/core/src/runtime/methods/subagent.ts:315),见子 Agent

--disallowedTools

写法由 extractDisallowedToolsArgs 解析(apps/zcode-cli/packages/cli/src/arguments.ts:127),它在全局参数解析之前先把这个选项摘出来:

  • 两种拼写 --disallowedTools--disallowed-tools,支持 --disallowedTools=Bash 的等号形式(arguments.ts:135);
  • 选项后面可以跟多个值,直到下一个以 - 开头的参数,一个都没有就报错(arguments.ts:147);
  • 值里的逗号和空格都是分隔符,但括号里的不算(arguments.ts:179);web_search 会被规范成 WebSearcharguments.ts:222)。

它有三处效果:注册内置工具和 MCP 工具时直接跳过(apps/zcode-cli/packages/core/src/tool/handlers/index.ts:202apps/zcode-cli/packages/core/src/mcp/index.ts:67),回合里向模型出示工具时再过滤一遍(apps/zcode-cli/packages/core/src/runtime/methods/turn-loop.ts:110),并投影进 PermissionService 的禁用名单(apps/zcode-cli/packages/bootstrap/src/app/app-config-options.ts:20)。注册时只取括号前的工具名(apps/zcode-cli/packages/core/src/tool/tool-visibility.ts:5),帮助文本也写明 "Bash(git *)" 会移除整个 Bash,不支持按命令内容匹配(apps/zcode-cli/packages/i18n/src/locales/zh-CN.ts:38)。它只作用于本次 -p 或 TUI 进程,不写进任何配置。

下一篇:生命周期 Hooks 与工作区信任——七个事件何时触发、进程钩子的输入输出协议,以及项目目录里的钩子为什么要逐条信任。

本页目录