AI 开发

AI Coding Agent 为什么需要权限控制?从 JSON Tool Call 到 Shell、Git、文件系统权限的完整安全模型

阅读约 16 分钟

安全篇写过攻击面。本篇只写权限模型:工具调用是一张 JSON,执行权在宿主。Shell、Git、文件系统是三道门。点「允许」不等于沙箱已经关上。

阅读前速览

门 锁什么 谁执行
Shell 命令字符串、复合命令 allow / ask / deny;沙箱另算
Git status、diff、commit、push;hooks / config 工作区可写 ≠ 仓库元数据可写
文件系统 src、.env、工作区外路径 工具规则 + OS 沙箱
网络 域名、出站 默认关;单独开名单

策略在宿主里评。模型填 JSON,不改门。

权限控制,是因为执行权不在模型手里

Coding Agent 调工具时,模型只产出一段 JSON:工具名加参数。真正跑 git status、改 src/orders/A-1042.json 的是宿主。Schema 管形状。权限策略管「过了形状,还准不准跑」。没有第二道门,第一道门只是礼貌。

  • Schema:锁参数。命令必须是字符串,路径必须落在约定前缀。
  • 权限策略:allow / ask / deny。先 deny,再 ask,再 allow。官方顺序如此,窄 allow 救不了宽 deny。
  • 沙箱:OS 再挡一层。提示注入绕过模型判断时,进程仍出不去工作区。点批准 ≠ 沙箱已开。

工具 JSON 怎么填,见 AI Coding Agent 与 JSON 工具调用。攻击面怎么收,见 Agent 安全危机与 JSON 验证。Schema 为什么是门,见 AI 为什么需要 JSON Schema。本篇不再复述那三条,只写 Shell / Git / 文件怎么落成对象。

权限对象:allow、ask、deny

Claude Code 把规则写进 settings.json 的 permissions。规则由宿主评,不是模型评。CLAUDE.md 只能改它想做什么,不能改宿主允不允许。虚构仓库,不要贴真实密钥:

{
  "permissions": {
    "allow": [
      "Bash(git status)",
      "Bash(git diff *)",
      "Bash(npm test *)",
      "Read(./src/**)",
      "Edit(./src/**)"
    ],
    "ask": [
      "Bash(git commit *)"
    ],
    "deny": [
      "Bash(git push *)",
      "Read(./.env)",
      "Edit(./.git/hooks/**)",
      "Edit(./.git/config)"
    ]
  }
}

光杆工具名(如 Bash)作 deny,会把工具从上下文摘掉。带括号的规则挡住匹配调用,工具仍在。复合命令按子命令分别匹配:允许 git status,不等于允许它后面再接别的命令。Codex 用权限档案(:workspace / :read-only),不要和旧的 sandbox_mode 混写。Cursor 用 permissions.json 的前缀名单,再加 sandbox.json。

第一道门:Shell

模型交出的是命令字符串。宿主先对策略,再决定跑不跑。只读查询通常过门:

{
  "type": "tool_use",
  "id": "call_7f2a",
  "name": "Bash",
  "input": {
    "command": "git status"
  }
}

同一套策略里,推远程被 deny。对象形状合法,执行权仍在宿主。Cursor 的终端名单按前缀匹配:git 能罩住 git status。官方也写明,Run Mode 是尽力护栏,不是硬边界。要硬边界,开沙箱。

{
  "type": "tool_use",
  "id": "call_8a01",
  "name": "Bash",
  "input": {
    "command": "git push origin main"
  }
}

第二道门:Git 不是普通写盘

工作区可写,不等于 .git 可写。Codex 在可写根下把 .git、.agents、.codex 默认收成只读;.git 若是 gitdir: 指针,解析后的目录同样保护。Claude Code 沙箱给 worktree 放行共享 .git 的提交所需写入,但 hooks/ 和 config 仍拒绝。Cursor 同样保护 .git/hooks 和 .git/config。推远程这一跳,宿主应当留下对象:

{
  "tool": "Bash",
  "command": "git push origin main",
  "eval_order": ["deny", "ask", "allow"],
  "matched": "Bash(git push *)",
  "rule": "deny",
  "sandbox": "workspace-write",
  "execute": false
}

失败就回 tool_result,写明命中哪条规则。不要静默改远程。commit 用 ask:人看见 diff 再过。push 用 deny,除非这条会话明确要发布。

{
  "type": "tool_result",
  "tool_use_id": "call_8a01",
  "is_error": true,
  "content": "denied by policy: Bash(git push *)"
}

第三道门:文件系统要两层

一层是工具规则。一层是 OS 沙箱。官方把两层分开写,是因为它们挡的不是同一种调用:

  • 工具规则:Claude 的 Read / Edit deny 管内置文件工具,以及它认出来的 cat / head 一类命令。任意语言脚本自己打开文件,这层罩不住。
  • OS 沙箱:进程级边界。默认只写当前工作区和会话临时目录。工作区外要另开路径。网络默认关。
  • 凭证文件:.env 进 deny。不要只靠「别读密钥」这句提示。

MCP 工具是另一张表,每个工具仍要单独过门。协议对比见 MCP Tool Schema vs Function Calling。本篇不写怎么调 MCP,只要求:接进来的工具,权限对象里要有名字。

五步把权限收成对象

不要从「请小心执行」开始。按门写:

  1. 先写 deny。push、hooks、config、.env、工作区外路径一次列清。
  2. 只读命令进 allow。status、diff、test。复合命令按子命令各写一条。
  3. 写盘和提交进 ask。人看见路径和 diff。会话结束,编辑批准不自动变成永久规则。
  4. 打开 OS 沙箱。Codex 用 :workspace,不要随手 :danger-full-access。Cursor 不要用 Run Everything 当默认。Claude 的 bypassPermissions 只留在隔离容器。
  5. 策略本身当 JSON 审。allow 和 deny 摊开对比。规则文件进版本库,本机覆盖进 local,不要混成一份说不清的名单。

Schema 怎么当门,见 Agents API + JSON Schema。本篇只要求:Coding Agent 上,每道门都有对象,而且由宿主评。

复盘权限,先摊开两段 JSON

一次越门,多半是规则没对齐,不是模型突然变野。

  • 把策略和那次 tool_use 格式化,看命中的是 deny 还是漏网的 allow。
  • 用 JSONPath 提取 抽出 $.permissions.deny、$.input.command。
  • 一次过门和一次被拒 JSON Diff。先改规则,再改提示。
  • Git 元数据规则和 src 写盘规则分开审。能改 A-1042.json,不表示能改 hooks。
  • 策略文件、仓库路径、会话日志不要贴进公共格式化站。JSONSort 在浏览器本地跑。

两段轨迹都能按规则重建,你才是在收权限,不是在收「我点过允许」。

常见误区

  • 以为 Schema 就是权限。Schema 管形状。合法的 git push 仍然要被 deny。
  • 只写 allow,不写 deny。官方顺序是 deny 先评。没有 deny,名单只是加速点批准。
  • 把工作区可写当成 Git 可写。hooks 和 config 是另一道门。官方默认保护它们。
  • 只用工具 deny,不开沙箱。脚本自己打开文件,工具规则罩不住。要进程边界。
  • 在笔记本上开 bypass / full-access / Run Everything。官方留给隔离环境。日常会话用 workspace + ask。
  • Codex 新旧两套一起写。出现 sandbox_mode,权限档案不生效。选一套。

FAQ

点「允许且不再询问」等于永久授权吗?

不一定。Claude Code 里,Bash 的永久批准写进仓库根的 settings.local.json;文件编辑的批准只活到会话结束。Cursor 的名单在 permissions.json。先看写下的对象,不要看按钮文案。

沙箱开了,还要 deny 吗?

要。沙箱挡进程摸到工作区外。deny 挡工作区里不该跑的命令,比如 push。两层锁的不是同一扇门。

Git 为什么单独列?

因为仓库元数据不是普通文件。能改 src,不该能改 hooks 和 config。push 是出站,不是写盘。官方把这几条从「工作区可写」里摘出来。

只读模式够不够?

够用来看代码。不够用来改 A-1042。Codex 的 :read-only、Claude 的 plan、Cursor 的只读沙箱,是探路档,不是发布档。

策略能进 Git 吗?

能,而且应该。组织共用的 deny / allow 进版本库。本机放宽进 local,不要提交。托管设置用来关掉 bypass 和 auto。

结论

Coding Agent 需要权限控制,因为模型只填 JSON。Shell、Git、文件系统是三道门。Schema 过了,宿主还要再评一遍。

先写清 git push 会撞上哪一条 deny。能指到路径,权限才算可验证。模型会换。门应该先稳定。

延伸阅读

更新记录: 首次发布