权限系统

Agent 系统作者:Claude Code 的权限系统是 7 种 mode × 8 种规则来源 × 11 种决策理由 × 10 种 hook 事件——拆开每一层的源码实现。

这一章的定位

权限系统回答的是**“能不能做”**——mode / rules / hooks / classifier 的信任决策层。

执行环境 正交——后者回答”在哪里做”(worktree / CCR / Seatbelt / bwrap 等物理隔离)。两个问题分章讲。

大多数 agent 产品的权限设计两个极端:全开(Devin 早期、Aider 默认)或 全问(疲劳 → 全部允许)。

Claude Code 走的第三条路:把权限做成一个多层的决策系统,在源码里是7 种 mode × 8 种规则来源 × 11 种决策理由 × 10 种 hook 事件的交叉产物。这一章把这个系统拆开。


Permission Modes:7 种,不是 4 种

源码里完整的 permission mode 集合types/permissions.ts):

export const EXTERNAL_PERMISSION_MODES = [
  'acceptEdits',
  'bypassPermissions',
  'default',
  'dontAsk',
  'plan',
] as const

export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'

对外 5 种 + 对内扩展 2 种 = 7 种模式。下面是完整的语义(源码 utils/permissions/PermissionMode.tsPERMISSION_MODE_CONFIG):

Permission Modes · 用户可见的信任状态机 四种模式,per-turn 可切换。状态栏持续显示当前模式。 用户可随时切换 · 对话中也可以 default 日常基线 自动 询问 破坏性 询问 Hooks 生效 何时用 日常工作。你在 每次变更落盘前 review agent 的修改 风险 acceptEdits 信任小改动 自动 自动 破坏性 询问 Hooks 生效 何时用 范围明确的任务 你信任 agent 做 增量改动的判断 风险 plan 只想不动 自动 禁止 破坏性 禁止 Hooks 生效 何时用 规划阶段 Agent 读 + 想 禁止任何副作用 风险 零(无写) bypassPermissions 高自主 · 危险 自动 自动 破坏性 自动 Hooks 生效 何时用 长时任务 已有 deny-list 兜底 范围已审核通过 风险 正交于 mode:settings.json 的 deny-list 始终生效 · hooks 始终运行 即使 bypassPermissions 也绕不过 deny 规则或拦截型 hook

模式别名语义风险
defaultDefault日常基线——读自动、写询问、破坏性询问
planPlan Mode只允许读 + TaskCreate,禁止一切写
acceptEditsAccept edits读写自动,破坏性询问
bypassPermissionsBypass全部自动——但 deny-list / hook 仍生效
dontAskDon’t Ask类似 bypass,语义上明确”不问我”
autoAuto modeant-only,feature-flagged TRANSCRIPT_CLASSIFIER——用 LLM 分类器判断每条命令可变(classifier 控)
bubble——内部模式,不对外暴露——

几个容易忽略的点

  • dontAsk vs bypassPermissions 不完全等价——它们的颜色 / UI 呈现不同,source 里定义的都是 color: 'error',但这是两条独立的模式常量。用户用 dontAsk 表达意图是”我关掉所有询问”,bypassPermissions 更像”全权允许”。两者的底层行为接近,但产品语义有别
  • auto 模式启用了 bash classifier:每条 bash 命令跑一个 YOLO classifier(源码 types/permissions.ts 第 346-397 行的 YoloClassifierResult)。分类器有 2 阶段(fast / thinking),完整遥测 input/output tokens、cache stats、request id——这是用 LLM 判断工具调用是否安全的实际实现
  • Plan mode 的符号是 PAUSE_ICON——UI 层面把它可视化为暂停状态,清晰告诉用户”我在看但不动”
  • acceptEdits / bypassPermissions / dontAsk 的符号都是 ⏵⏵——fast-forward 含义,跟视频播放器快进符号一致

给自研 agent 的启示权限 mode 不只是 config,是 UI 状态。用户切换 mode 时应该有视觉反馈(颜色、符号、状态栏),否则他们很快会忘记自己处于什么信任级别。


8 种规则来源

PermissionRuleSource (types/permissions.ts) 定义了规则来源的严格枚举

export type PermissionRuleSource =
  | 'userSettings'      // ~/.claude/settings.json
  | 'projectSettings'   // <repo>/.claude/settings.json(进 git)
  | 'localSettings'     // <repo>/.claude/settings.local.json(不进 git)
  | 'flagSettings'      // CLI --allowedTools / --disallowedTools
  | 'policySettings'    // 企业 policy(Managed settings)
  | 'cliArg'            // 命令行参数
  | 'command'           // /permissions 等命令追加的
  | 'session'           // 本 session 内 "always allow" 选的

分层的含义

  • userSettings / projectSettings / localSettings——跟记忆系统对齐的三层(见 记忆系统
  • policySettings——企业 / IT 部门的强制规则。policy 可以覆盖所有其他 source,用于合规
  • flagSettings / cliArg——命令行传入的 one-off 规则
  • session——用户在对话中点过 “always allow X” 追加的规则,会话结束就丢
  • command——通过 /permissions slash command 交互追加的

每条 PermissionRule 携带自己的 source,规则冲突时可以溯源——audit 时能回答”这条 allow 是从哪来的”。

给自研 agent 的启示:权限规则不要只存最终值,存 (value, source) 元组。安全事故 / 合规审计的时候你会需要溯源。


11 种决策理由

一条权限决策是 allow / ask / deny 之一,但为什么是这个决策——PermissionDecisionReason 有严格的 11 种分类(types/permissions.ts 第 271-324 行):

export type PermissionDecisionReason =
  | { type: 'rule'; rule: PermissionRule }
  | { type: 'mode'; mode: PermissionMode }
  | { type: 'subcommandResults'; reasons: Map<string, PermissionResult> }
  | { type: 'permissionPromptTool'; permissionPromptToolName: string; toolResult: unknown }
  | { type: 'hook'; hookName: string; hookSource?: string; reason?: string }
  | { type: 'asyncAgent'; reason: string }
  | { type: 'sandboxOverride'; reason: 'excludedCommand' | 'dangerouslyDisableSandbox' }
  | { type: 'classifier'; classifier: string; reason: string }
  | { type: 'workingDir'; reason: string }
  | { type: 'safetyCheck'; reason: string; classifierApprovable: boolean }
  | { type: 'other'; reason: string }

每种类型表达不同的决策源头:

类型含义
rule匹配了某条显式规则(allow / ask / deny)
mode当前 permission mode 的默认行为
subcommandResults复合命令(cmd1 && cmd2)——每个子命令单独判断,整体合取
permissionPromptTool外部 “permission prompt tool” 返回了决策
hookPreToolUse hook 返回 permissionDecision
asyncAgent异步 agent 分类器判断
sandboxOverride命令被标为”无法 sandbox”或用户 --dangerously-disable-sandbox
classifierYOLO bash classifier 判断
workingDir命令要访问的路径不在 allowed dirs 内
safetyCheck静态安全检查(敏感文件、Windows 路径 bypass、跨机器 bridge 等)
other兜底

为什么这么多类型审计 + 解释性。当用户问”为什么这条命令被 deny 了”,UI 能展开完整的决策路径——不是一句”blocked”,是”因为 policySettings 里的 deny 规则 X 匹配了”。

给自研 agent 的启示决策产物(allow / deny)和决策理由(11 种之一)要分离。前者是二元的,后者让你能解释、追查、重跑。


Hook 系统:10 种事件

源码里有10 种 hook 事件,不是我之前写的 5 种(utils/hooks.ts 里的 hook event name 枚举):

事件触发时机
PreToolUse工具调用前
PostToolUse工具成功返回后
PostToolUseFailure工具失败返回后(新增)
UserPromptSubmit用户发送消息时
Stop每轮结束时
StopFailure每轮异常结束
SubagentStop子 agent 结束时
PreCompact压缩前(见 压缩
PostCompact压缩后
WorktreeCreate创建 worktree 时

新增的事件值得注意

  • PostToolUseFailure:工具失败时单独触发的 hook——让你可以对失败专门做处理(比如记 Sentry、发告警),而不和成功路径混在一起
  • StopFailure / SubagentStop:区分了主 agent 和子 agent 的 stop 事件
  • WorktreeCreate:worktree 创建时触发——可以在这里跑 setup script(拉 deps、准备 sandbox 等)

Hook 输出的完整 JSON schema

源码 utils/hooks.ts 第 415-444 行暴露了 hook 返回的完整 schema

{
  continue: boolean,                // 是否继续(false → 中断 agent 循环)
  suppressOutput: boolean,           // 是否抑制 hook 的 stdout
  stopReason: string,                // 中断原因(显示给用户)
  decision: '"approve" | "block"',   // 简化决策
  reason: string,                    // 决策原因
  systemMessage: string,             // 注入给 agent 的 system message
  permissionDecision: '"allow" | "deny" | "ask"',
  hookSpecificOutput: {
    // 每个 event 的专属字段:
    'PreToolUse':     { hookEventName, permissionDecision, permissionDecisionReason, updatedInput },
    'UserPromptSubmit': { hookEventName, additionalContext },
    'PostToolUse':    { hookEventName, additionalContext },
  }
}

几个值得深挖的字段

  1. updatedInput(仅 PreToolUse):hook 可以修改工具调用的参数。例子:改写 Bash 命令在运行前 filter 掉敏感 output、改写 Write 内容加 header、改写 Read 的路径指向 sandbox 版本。这是非常强大的能力——hook 不只是 allow/deny,是可拦截可修改
  2. additionalContext(UserPromptSubmit / PostToolUse):hook 可以注入额外上下文到 prompt / conversation 里。例子:“用户提交了 prompt 后追加当前 CI 状态”、“工具成功后追加相关文档”
  3. continue: false:hook 可以打断整个 agent 循环,不只是拒绝当前操作
  4. systemMessage:hook 可以向 agent 注入 system-level 指令——这是 hook 能力的最强表现,等于临时扩展 system prompt

Hook 超时TOOL_HOOK_EXECUTION_TIMEOUT_MS = 10 * 60 * 1000 —— 10 分钟。这个长度暗示 hook 是一等公民,不是快速拦截器。它可以跑 lint、跑 test、发邮件、做复杂分析。

Hook 非 JSON 输出的处理

Hook 输出可以是 JSON 也可以是 plain text(源码 parseHookOutput):

  • JSON:按上面的 schema 解析,走结构化路径
  • Plain text(不以 { 开头):原文注入为 additionalContext

这降低了写 hook 的门槛——简单的 hook 直接 echo 一行到 stdout 就行

给自研 agent 的启示hook 接口要支持三个层次——简单的 plain text → 中等的 JSON decision → 复杂的 updatedInput + systemMessage。门槛低、上限高,用户按需升级。


进程级沙箱属于执行环境层

本章不讲进程级沙箱实现(macOS Seatbelt / Linux bwrap+seccomp / Windows 无沙箱)——那是 执行环境 的话题。本章只讲权限决策层。

两层的交互:权限层决定”要不要做”,沙箱层决定”做的时候能不能踩到敏感资源”。bypassPermissions mode 把权限层完全打开,但沙箱层仍然生效——bwrap 不会因为 bypassPermissions 就让你写 /etc/passwd。这是 四层防线 讨论的核心。


Bash DANGEROUS_PATTERNS:auto mode 的 allowlist 保护

utils/permissions/dangerousPatterns.ts 定义了一个危险 bash 前缀清单,用于 auto mode 启动时自动剥离过于宽松的 allow 规则:

export const CROSS_PLATFORM_CODE_EXEC = [
  // 解释器
  'python', 'python3', 'python2', 'node', 'deno', 'tsx',
  'ruby', 'perl', 'php', 'lua',
  // Package runners
  'npx', 'bunx', 'npm run', 'yarn run', 'pnpm run', 'bun run',
  // Shells
  'bash', 'sh',
  // 远程
  'ssh',
] as const

export const DANGEROUS_BASH_PATTERNS: readonly string[] = [
  ...CROSS_PLATFORM_CODE_EXEC,
  'zsh', 'fish', 'eval', 'exec', 'env', 'xargs', 'sudo',
  // Ant-only 扩展
  ...(process.env.USER_TYPE === 'ant' ? [
    'fa run', 'coo',               // Ant 内部工具
    'gh', 'gh api', 'curl', 'wget',  // 网络 / 外泄
    'git',                          // git config core.sshCommand = 任意代码
    'kubectl', 'aws', 'gcloud', 'gsutil',  // 云资源写
  ] : []),
]

设计意图(源码注释):

An allow rule like Bash(python:*) or PowerShell(node:*) lets the model run arbitrary code via that interpreter, bypassing the auto-mode classifier.

翻译:如果用户写了 Bash(python:*) 这样的 allow 规则,模型就可以通过 python 跑任意代码——绕过了 bash classifier 的审查。这种”表面限制但实际放任”的规则在 auto mode 入口被显式剥离

ant-only 的扩展清单特别值得注意git 也在里面——因为 git config core.sshCommand 能跑任意代码,git 的 allow prefix 不安全。这是从对抗者视角出发的 pattern 清单,不是信任 command name 字面意思。

给自研 agent 的启示allowlist 不能只看前缀——要考虑每个前缀实际能触达的执行路径。python:* / node:* 这种本质是”允许任意代码”的前缀,应该 deny 或要求 classifier 逐条判断。


Path Pattern:4 种权限规则的路径语法

Claude Code 的权限规则支持的路径语法有 4 种(utils/sandbox/sandbox-adapter.ts 第 83-97 行):

前缀含义
//path绝对路径(转换为 /path
/path相对于 settings 文件所在目录(展开为 $SETTINGS_DIR/path
~/path用户 home 目录(由 sandbox-runtime 处理)
./pathpath相对路径(由 sandbox-runtime 处理)

/path 这个语义是 Claude Code 特有的——以单斜线开头不代表绝对路径,代表”相对于我这个 settings.json 所在目录”。这让一个 settings 文件可以在不同机器上复用,只要目录结构相对一致。

给自研 agent 的启示设置的路径语法要支持相对于配置文件本身——绝对路径在跨机器 / 容器 / CI 场景会崩。提供一个 “相对于 settings” 的约定能解决 80% 的配置漂移问题。


四层防线:纵深防御总图

Claude Code 的安全模型本章内四层同时生效,再叠加 执行环境 层的第五层:

权限决策层(本章):
  1. Mode            —— 当前 permission mode 的默认行为
  2. Rules           —— allow / deny / ask 规则(按 source 优先级合并)
  3. Classifier      —— auto mode 下 bash LLM 分类器判断
  4. Hooks           —— PreToolUse hook 可以拦截 / 修改 / 注入 context

执行环境层(另一章):
  5. Platform Sandbox —— Seatbelt / bwrap 进程级隔离

任何一层拒绝 → 操作被拒。这是安全的”合取”模型:

  • mode 是 default 但 rule 说 allow → 允许(rule 覆盖 mode 默认)
  • mode 是 bypassPermissions 但 rule 说 deny拒绝(deny 始终生效)
  • mode allow、rule allow,但 hook 返回 deny → 拒绝
  • 一切都 allow,但 bwrap 阻止了文件写 → 拒绝(执行环境层兜底)

bypassPermissions 不能绕过 hook,也不能绕过进程沙箱——这是关键设计。“最大权限”mode 只放开默认行为,不放开显式规则、hook 拦截、OS 级隔离

源码上的佐证:permissionDecision 的优先级合并逻辑里,deny 始终胜过 allow——这是fail-closed 默认。


--dangerously-skip-permissions:最后的逃生舱

这是一个刻意起得难看的 CLI flag,绕过整个权限系统。两层设计意图:

  1. 用户必须主动传——不是默认开启
  2. 名字本身就是警告——不叫 --auto-approve,叫 “dangerously”

配合源码 sandboxOverride 决策理由里的 'dangerouslyDisableSandbox'——任何通过这个 flag 豁免的决策,理由都明确记录。

给自研 agent 的启示危险的逃生舱命名要违反 UX 惯例——故意不友好。让用户 every time 都要多想一秒。


给自研 agent 的要点

  1. 权限 mode 是 UI 状态,不是 config。7 种 mode 各自有符号 / 颜色 / UI 呈现,用户切换时必须有视觉反馈
  2. 规则来源要枚举化:userSettings / projectSettings / policySettings / cliArg / session / command——合规审计的时候你会需要溯源
  3. 决策理由分类要细:11 种 PermissionDecisionReason 让 “为什么 deny” 可解释。二元决策 + 多种理由是对的抽象
  4. Hook 要三层 API:plain text(简单)→ JSON decision(中等)→ updatedInput + systemMessage(高级)。降低门槛 + 高上限
  5. Hook 不只是 yes/no,还可以 修改参数注入 system message打断 agent 循环——是一等扩展面
  6. 10 分钟 hook 超时:hook 可以跑 lint / test / 复杂分析,不是快速拦截器
  7. 权限层和执行环境层是正交的两层——bypassPermissions 跳过前者不跳过后者。见 执行环境
  8. Bash classifier 用 LLM 判断命令安全性是个方向——2 阶段(fast/thinking)+ 完整遥测,值得学
  9. DANGEROUS_PATTERNS 是对抗者视角——python:* 允许 prefix 等于允许任意代码。allowlist 设计要看实际执行路径不是字面名字
  10. Deny 始终胜过 allow,fail-closed 默认。bypassPermissions 只绕过默认行为,不绕 deny list 和 hook
  11. 危险入口的命名违反 UX 惯例——--dangerously-skip-permissions 是故意难看的,让用户停一下
  12. 路径语法支持相对于 settings 文件——跨机器 / CI / 容器 workflow 的必需特性

延伸阅读

  • Claude Code 源码:types/permissions.tsutils/permissions/utils/hooks.tsutils/permissions/dangerousPatterns.ts
  • 执行环境——权限之外的物理隔离层(worktree / CCR / bwrap)
  • 系统提示词组装——tool schema 本身也是 prompt 的一部分
  • 设计启示——把本章的原则抽象到”hook vs prompt”的一般化讨论
这页有帮助吗?