Claude Code 101|07|Permissions
Claude Code 的权限系统不是一个确认弹窗,而是 Read/Edit/Bash/MCP/Web 等不同风险面的组合治理。
代码 Agent 的安全边界不在模型输出文本,而在 runtime 是否允许它改变世界。一个回答错误的聊天模型最多误导用户;一个拥有文件写入、shell、网络和外部工具的 Coding Agent,则可能删除文件、泄露信息、污染配置、执行恶意命令,或者把危险授权保存成长期规则。
所以,Permissions 不是“弹一个确认框”。确认框只是用户交互形态,真正的权限系统是一套风险建模:哪些行动是读,哪些是写,哪些会改变未来执行环境,哪些会访问开放世界,哪些命令看似只读但包含重定向或命令替换,哪些路径因为 symlink、UNC、大小写或特殊设备绕过而变危险。
Claude Code 很适合作为 Permissions 案例:文件权限、Bash 权限、通用规则、规则解析、工具执行和 hooks 一起组成 runtime 的安全模型。核心不是“弹一个确认框”,而是把不同工具的行动风险拆成可解释、可配置、可审计的决策流程。
工程问题:Agent 行动风险必须被建模
权限系统要回答的问题不是“用户是否信任模型”,而是“这次行动的风险面是什么”。同一个 Agent 可以做很多事:读一个业务代码文件、写一个配置文件、运行 git status、运行 rm -rf、访问文档网站、调用 MCP server、通过 LSP 查引用。这些行动不能用同一个 Allow/Deny 规则粗暴处理。
至少有五类风险需要分开建模。
第一,信息暴露。Read、Grep、Glob、LSP、WebFetch 都可能扩大模型可见信息。只读不等于无风险,特别是读取凭据、私密配置、gitignored 文件或用户 home 下敏感路径时。
第二,状态修改。Edit、Write、NotebookEdit、Bash 写操作会改变文件或环境。写操作需要比读操作更严格的路径检查、staleness 检查和用户确认。
第三,未来执行环境污染。有些文件本身不是业务代码,但会改变之后的 shell、git、编辑器、MCP 或 Agent 行为,例如 .bashrc、.zshrc、.gitconfig、.mcp.json、.claude.json、.git、.vscode、.idea、.claude。修改这些文件的风险大于普通业务代码。
第四,命令执行。Bash 是最大风险面,因为字符串命令可以组合管道、重定向、子 shell、环境变量、命令替换、控制流和外部程序。权限系统如果只做 startsWith('git status'),一定会被绕过。
第五,开放世界访问。Web 和 MCP 可能连接项目之外的世界。它们的风险既包括数据外泄,也包括供应链和远程能力调用。
成熟权限系统的目标不是永远拒绝这些行动,而是把它们拆成可解释、可配置、可审计的决策。
概念边界:Permissions 是 Tool Runtime 的治理子协议
上一篇讲 Tool Runtime 时,我们看到每个 Tool 都有 checkPermissions(),工具执行链会在 validate 之后进入权限判断。Permissions 因此不是全局拦截器,而是工具协议的一部分。
它又不完全属于单个工具。通用 deny / ask / allow 规则、permission mode、session-scoped 授权、hook permission、用户交互、规则保存建议,都需要跨工具统一。于是 Claude Code 的权限设计呈现为“通用规则 + 工具特定判断 + 用户交互 + 持久规则”的组合。
下一篇会进入 File、Bash、Web、LSP 四类工具。那些工具之所以复杂,很大程度上正是因为它们的权限边界不同。Permissions 先建立风险模型,具体工具再把模型落到文件路径、命令 AST、domain、LSP location 等细节上。
机制一:Read 和 Edit 是两条不同权限通道
文件系统权限最容易被低估。很多系统只有“是否允许访问文件”一个开关,但 Claude Code 把 Read 和 Edit 分开。src/utils/permissions/filesystem.ts 中有 checkReadPermissionForTool() 和 checkWritePermissionForTool() 两条入口。它们共享一些路径规范化和规则匹配逻辑,但风险语义不同。
Read 权限关注信息暴露。它要处理原始路径和 resolved path,避免 symlink 绕过;要阻断 UNC 路径和可疑 Windows path pattern;要处理工作目录内默认可读与敏感路径额外保护之间的关系;还要尊重显式 deny / ask / allow 规则。读取一个业务代码文件和读取一个凭据文件,不应该被同等对待。
Edit 权限关注状态修改。它除了路径规则,还要检查危险文件和目录、.claude 相关特殊规则、session-scoped 授权、acceptEdits 模式、显式规则和默认 ask。允许编辑有时可以隐含读取,因为写入前需要理解文件内容;但 read-specific deny / ask 仍然可能优先,不能因为写权限就绕过读边界。
这个区分非常关键。读和写在工程系统里不是对称操作:读会扩大模型知识面,写会改变用户世界。二者都危险,但危险类型不同。一个好的权限系统应该把它们建模成不同 action,而不是同一个 file:*。
可迁移抽象如下:
type FileAction = 'read' | 'write' | 'create' | 'delete'
type FilePermissionInput = {
action: FileAction
requestedPath: string
resolvedPath: string
toolName: string
sessionId: string
}
async function decideFilePermission(input: FilePermissionInput, ctx: PermissionContext) {
const normalized = normalizeForSecurity(input)
if (isSuspiciousPath(normalized)) return deny('suspicious path')
const explicit = matchRules(input.action, normalized, ctx.rules)
if (explicit) return explicit
if (input.action === 'read') return decideRead(normalized, ctx)
return decideWrite(normalized, ctx)
}
注意这里同时保留 requested path 和 resolved path。权限系统必须处理“用户/模型请求的路径”和“文件系统真实指向的路径”之间的差异。
机制二:危险路径代表“未来控制面”
src/utils/permissions/filesystem.ts 中定义了 DANGEROUS_FILES 和 DANGEROUS_DIRECTORIES。文件包括 .gitconfig、.gitmodules、.bashrc、.bash_profile、.zshrc、.zprofile、.profile、.ripgreprc、.mcp.json、.claude.json;目录包括 .git、.vscode、.idea、.claude。
这组名单的意义不在于“这些名字特殊”,而在于它们代表未来控制面。
- shell rc 文件会影响之后命令执行。
- git config 和 submodule 配置会影响版本控制行为和供应链。
- MCP 配置会影响外部工具连接。
- Agent 配置会影响后续权限和运行方式。
- 编辑器配置可能影响任务、调试、插件和自动化。
.git目录直接关系仓库历史与引用。
也就是说,权限系统不只要保护当前文件内容,还要保护未来 runtime 的控制面。攻击者如果无法直接执行危险命令,也可能通过修改配置让下一次安全命令变危险。Agent 如果不理解这一点,就会把“修改配置文件”误判为普通文本编辑。
伪代码可以这样写:
function classifyPathRisk(path: NormalizedPath): PathRisk {
if (matchesDangerousControlFile(path)) {
return { level: 'high', reason: 'modifies future execution context' }
}
if (isCredentialLike(path)) {
return { level: 'high', reason: 'may expose secrets' }
}
if (isProjectSource(path)) {
return { level: 'normal', reason: 'project source' }
}
return { level: 'unknown', reason: 'outside known workspace' }
}
危险路径列表不应该被看作硬编码偏好,而应该被看作风险分类器的一部分。不同组织可以扩展这组规则,比如 CI 配置、deployment 配置、package manager hooks、Terraform state 等。
机制三:路径安全是现实细节,不是字符串前缀
文件权限之所以复杂,是因为路径不是普通字符串。src/utils/permissions/filesystem.ts 里能看到大小写规范化、路径展开、path traversal、UNC、Windows path、symlink resolved path、macOS/Windows case-insensitive filesystem 等相关处理。注释中特别提到,把路径统一小写用于比较,是为了避免在大小写不敏感文件系统上用混合大小写绕过安全检查。
这类细节在安全系统里非常重要。常见绕过包括:
- 使用 symlink 指向工作区外敏感路径。
- 使用大小写变化绕过
.claude/.git检测。 - 使用
..或路径分隔符差异绕过目录限制。 - 使用 UNC 或 Windows 特殊路径触发非预期访问。
- 通过设备文件、临时目录或工具结果目录制造循环读取。
权限系统不能只做 path.startsWith(projectRoot)。安全判断应该基于规范化路径、resolved path、平台语义和规则来源。
可迁移流程如下:
async function normalizeForPermission(rawPath: string): Promise<SecurePath> {
const expanded = expandUserAndEnv(rawPath)
const sanitized = rejectTraversal(expanded)
const resolved = await resolveSymlinks(sanitized)
const comparable = caseFoldForSecurity(resolved)
if (isUncOrDevicePath(comparable)) {
throw new PermissionError('unsupported path form')
}
return { raw: rawPath, expanded, resolved, comparable }
}
注意 caseFoldForSecurity 不等于改变实际执行路径。权限比较可以用安全规范化形式,但实际工具执行仍应使用经过验证的真实路径,避免破坏用户路径语义。
机制四:Bash 权限必须理解命令结构
Bash 是权限系统中最复杂的部分。src/tools/BashTool/bashPermissions.ts 显示了多阶段决策:先处理 deny / ask 规则,再检查路径约束,再处理 exact allow 和 prefix allow,再检查 sed 约束、permission mode、read-only,最后没有匹配时要求用户批准。src/tools/BashTool/bashSecurity.ts 和 src/tools/BashTool/readOnlyValidation.ts 则承担命令安全与只读判断。
关键点是:Bash 权限不是字符串匹配。它需要理解命令结构。
一个命令可能表面以 git status 开头,但后面接了 ; curl ...;可能通过 &&、||、管道和重定向改变语义;可能在命令替换里执行额外代码;可能先 cd 到另一个目录再运行看似安全的命令;可能用 sed -i 或重定向修改文件;可能通过环境变量或 shell builtin 改变执行上下文。
Claude Code 使用 tree-sitter 解析命令,并在解析失败或结构复杂时倾向 fail closed:请求用户确认。bashPermissions.ts 中也能看到 AST parse 成功时,可以避免某些 legacy regex 检查带来的误报;但当无法可靠理解命令结构时,就不应该自动放行。
这体现了安全系统的基本原则:能证明安全才自动允许,不能证明就进入 ask,而不是“没发现危险就允许”。
伪代码如下:
async function decideBashPermission(command: string, ctx: PermissionContext) {
const ast = parseShellCommand(command)
const explicit = matchExplicitRules(command, ctx.rules)
if (explicit?.behavior === 'deny') return explicit
if (explicit?.behavior === 'ask') return explicit
if (!ast.ok) {
return ask('command structure cannot be safely analyzed')
}
const structuralRisk = inspectShellAst(ast.tree)
if (structuralRisk.requiresApproval) return ask(structuralRisk.reason)
const pathRisk = checkCommandPathConstraints(ast.commands, ctx)
if (pathRisk.requiresApproval) return ask(pathRisk.reason)
if (isReadOnlyCommand(ast.tree)) return allow('read-only command')
const savedRule = matchAllowRules(ast.commandPrefix, ctx.rules)
if (savedRule) return allow('matched saved rule')
return ask('command requires approval')
}
这段伪代码和真实实现的函数名不同,但表达的是同一架构:规则匹配、结构解析、路径约束、只读分类和默认 ask 必须组合。
机制五:权限规则建议不能制造长期风险
权限系统不仅要判断本次调用,还要处理“以后不要再问”的规则保存。这里有一个很容易被忽略的问题:如果系统给用户建议了过宽规则,用户一键保存后,风险会从一次行动扩大成长期授权。
Claude Code 对 Bash 规则建议非常谨慎。它避免建议 bash:*、sh:*、sudo:*、xargs:*、env:* 等过宽前缀;对 compound command,也限制建议数量。bashPermissions.ts 中的 suggestion 逻辑体现了这一点:可以建议 exact command 或较窄 prefix,但不能为了减少弹窗把安全边界放得太宽。
这背后的原则是:权限规则本身也是一种代码。它会改变未来 runtime 的行为,因此必须被当作高风险配置处理。一个坏规则比一次坏命令更危险,因为它会沉默地允许后续行动。
可迁移设计:
type PermissionSuggestion = {
scope: 'once' | 'session' | 'project' | 'global'
pattern: string
risk: 'narrow' | 'medium' | 'broad'
}
function suggestSavedRules(action: Action): PermissionSuggestion[] {
const candidates = deriveNarrowPatterns(action)
return candidates.filter(s => s.risk !== 'broad')
}
UI 也应该把 “allow once” 与 “allow always” 清楚地区分。很多权限事故并不是用户批准了危险操作,而是用户以为批准一次,实际保存了长期规则。
机制六:Hooks 可以参与权限,但不能绕过交互语义
Tool Runtime 中还有 hooks。src/services/tools/toolHooks.ts 处理 hook permission result,并把 allow、deny、ask 与工具要求交互的情况组合起来。hook 可以提供额外决策,但 runtime 必须确认它是否满足工具交互需求,不能让 hook 的 allow 盲目绕过所有约束。
这说明权限系统不仅是用户和工具之间的关系,还包括自动化策略。企业或团队可能希望通过 hooks 统一拒绝某些工具、自动允许某些只读操作、或在执行前做审计。这样的扩展能力很有价值,但也会带来治理复杂度。
原则是:hook 决策应该进入同一 PermissionResult 协议,而不是旁路执行。它要能表达 allow、deny、ask、updatedInput、decisionReason,并被 transcript / analytics / UI 观察到。否则权限系统会被多个隐形通道撕裂。
机制七:MCP 与 Web 是开放世界边界
MCP 工具通常以 server/tool 粒度进入权限系统。外部 server 暴露的 annotations 可以告诉 runtime 某个工具是 readOnly、destructive 还是 openWorld,但这不是天然信任。权限应该分两层:是否允许连接某个 MCP server,以及是否允许调用某个 server 的某个 tool。
WebFetch 则按 domain 做权限控制。预批准文档站点可以直接允许抓取,但跨 host redirect 不应该自动继续,因为 redirect 可能把用户批准的 URL 带到另一个域名。WebSearch 又是另一类边界,它往往通过模型 API 的 server-side search 能力工作,权限语义与本地 fetch 不同。
开放世界权限的关键不是“能不能联网”一个开关,而是:访问哪个 domain、以什么方式访问、是否可能上传本地信息、redirect 是否改变授权对象、结果是否进入模型上下文、是否被记录。
Claude Code 作为 Permissions 案例
从 Agent Runtime 的角度看,Claude Code 的权限系统不是一个 UI 弹窗,而是一组工具风险面的组合治理。文件读写、Bash、Web、MCP、LSP、hooks 和长期规则都对应不同风险模型,不能用同一个 allow / deny 开关粗暴处理。
这也是 Permissions 的关键:安全感不是让模型“自觉谨慎”,而是 runtime 能够把行动风险建模、解释、授权、记录和复用。
工程启发
第一,把权限建模为 action risk,而不是 yes/no。Read、Edit、Bash、Web、MCP、LSP 的风险不同,应该有不同决策路径。
第二,路径安全要处理真实文件系统细节。symlink、大小写、UNC、设备文件、path traversal、工作区边界都必须在代码里防御。
第三,命令权限要理解结构。对 shell 字符串做前缀匹配是不够的,至少要有 AST、重定向、控制流、命令替换、路径参数和只读分类。
第四,保存权限规则比批准单次操作更危险。规则建议应该尽量窄,避免产生长期过宽授权。
第五,hook 和外部策略必须进入同一权限协议。不要允许旁路自动化绕过审计与 UI。
第六,只读不等于无风险。读取敏感文件、LSP 查询、Web 抓取、MCP read-only tool 都可能扩展模型可见信息。
小结
Claude Code 的 Permissions 把 Agent 行动拆成不同风险面:文件读取、文件写入、危险路径、shell 命令、开放世界访问、外部工具调用、hook 决策和长期规则。真正的安全感不是让模型“自觉谨慎”,而是 runtime 能够在代码层把风险建模、验证、解释和审计。
下一篇会把这些权限原则落到具体工具边界上:File、Bash、Web、LSP。它们共享 Tool Runtime,也共享权限协议,但每一类工具连接真实工程环境的方式都不同。