Claude Code 权限系统到底是怎么工作的
实现说明
本文基于 2026 年年中观察到的 Claude Code 行为、本地文件布局以及插件源码整理而成。这些属于实现细节,会随版本变化。文中在合适的地方会引用 Codex CLI 做对比。
当你看到 Claude Code 询问“是否允许此工具调用?”并点击“批准”时,提示背后其实还有几件事在同时发生:静态规则、插件钩子、模式匹配,以及一些会随着你批准的操作越来越多而不断增多的本地文件。
这很重要,因为 Claude Code 不只是一个聊天窗口。它会在你的工作站上读取文件、写代码、执行 shell 命令、发起网络请求、派生子代理,还能管理各种任务。它用的是你的凭据和你的 shell 环境。权限系统就是模型请求与机器实际执行之间的主要闸门。
三种权限决定
每次工具调用都会归入以下三类之一:
- 允许(Allow)——立即运行。
- 拒绝(Deny)——阻止执行,并把错误返回给模型。
- 询问(Ask)——停下来询问用户。
这些决定由多个来源按优先级顺序综合评估;任何来源的“拒绝”都会覆盖其他来源的“允许”。当没有规则匹配时,默认行为取决于当前权限模式。
权限规则保存在哪里
Claude Code 会从两个位置读取权限规则,并合并使用:
全局用户设置,位于 ~/.claude/settings.json:
{
"permissions": {
"allow": [
"Bash(cargo check)",
"Bash(cargo build --release)",
"Bash(cargo run:*)",
"Bash(cargo build:*)"
],
"deny": [],
"ask": [],
"defaultMode": "default"
}
}
项目级设置,位于工作目录下的 .claude/settings.local.json:
{
"permissions": {
"allow": [
"Bash(npm run:*)",
"Bash(npx tsc:*)",
"Bash(bun add:*)",
"Read(/mnt/disks/data-disk/code/rye/web/**)",
"WebFetch(domain:github.com)"
]
}
}
针对同一条规则模式,项目设置会覆盖全局设置。两个文件使用相同的规则格式。
规则模式语法
权限规则遵循 ToolName(pattern) 的格式,其中 pattern 的具体写法取决于工具:
Bash(cargo check) # Exact command match
Bash(cargo:*) # Prefix match: any cargo subcommand
Bash(npm run:*) # Prefix: npm run with any arguments
Read(/repo/**) # Recursive glob: any file under /repo
WebFetch(domain:github.com) # Domain restriction for web fetches
Bash 模式末尾的 * 通配符用来匹配任意剩余参数;Read 和 Write 模式中的 ** 表示递归匹配任意层级;限定域名的 WebFetch 规则则用来限制助手可以访问哪些主机。
当你以交互方式批准一次工具调用时,Claude Code 可能会询问是否要「始终允许」。如果同意,新规则就会追加到 settings.local.json 中。时间一长,这个文件会越来越大,记录下你在这个项目里批准过的所有操作。
Hook 系统
除了静态规则,Claude Code 还通过插件提供了一套 Hook(钩子)系统。Hook 本质上是 shell 命令,它们会在工具调用生命周期的特定事件触发时执行:
PreToolUse——在工具调用执行前触发,可以允许、拒绝或强制弹出询问。PostToolUse——在工具调用执行完毕后触发,可以记录日志、发出提示或修改结果。UserPromptSubmit——在用户提交提示词时触发,可以校验或转换输入。Stop——在会话即将结束时触发,可以阻止会话提前终止。
PreToolUse 钩子会把工具名称和输入参数以 JSON 格式通过标准输入(stdin)传给它:
{
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/build-artifacts",
"timeout": 120000
},
"hook_event_name": "PreToolUse"
}
钩子随后可以返回一个权限决定:
{
"hookSpecificOutput": {
"permissionDecision": "deny"
},
"systemMessage": "Recursive deletion blocked by policy."
}
Hook 在插件配置文件中注册,以子进程方式运行。超时时间一般很短(10 秒)。如果某个 Hook 报错或超时,系统会采取「失败即放行」的策略——工具调用照常执行,就像这个 Hook 不存在一样。
Hookify:用 Markdown 写规则
官方的 hookify 插件在此基础上扩展出了一套规则引擎:它使用带 YAML frontmatter 的 Markdown 文件来定义规则:
---
name: block-dangerous-rm
enabled: true
event: bash
action: block
pattern: rm\s+-rf
---
Dangerous recursive deletion detected.
更复杂的规则支持多个条件同时判断:
---
name: block-env-secrets
enabled: true
event: file
action: block
conditions:
- field: file_path
operator: regex_match
pattern: \.env$|credentials
- field: new_text
operator: contains
pattern: API_KEY
---
这类规则以 .claude/hookify.*.local.md 文件的形式存放在项目目录中,支持 regex_match、contains、equals、starts_with、ends_with、not_contains 等操作符,可以作用于 command、file_path、new_text、old_text、user_prompt 这些字段。
作为本地机制,这套方案确实方便,但也有个明显的局限:它只在本地生效。团队没有一份集中的规则清单,规则变更没有内置的审计记录,你也无法轻易证明某条规则在某个会话中是否真的生效过。
批准是如何一点点堆积起来的
风险是慢慢显现的。
每当开发者在批准某个工具调用时点下「始终允许」(always allow),项目权限文件里就会追加一条新规则。几周下来,这个文件会自然地不断膨胀:
{
"permissions": {
"allow": [
"Bash(cargo:*)",
"Bash(sqlite3:*)",
"Bash(npm run:*)",
"Bash(npx tsc:*)",
"Bash(bun add:*)",
"Bash(wc:*)",
"Bash(xargs ls:*)",
"Bash(xargs cat:*)",
"Read(/mnt/disks/data-disk/code/rye/web/**)",
"WebFetch(domain:github.com)",
"WebFetch(domain:claude.ai)"
]
}
单看每一条批准,在当时可能都合情合理。但把它们放在一起,就成了一张不断扩大的白名单,而几乎没有人会去审查它。
对比 Codex
Codex CLI 也有同样的模式,只是换了一种表达方式。它的 ~/.codex/rules/default.rules 文件会持久化前缀匹配的批准规则:
prefix_rule(pattern=["rg"], decision="allow")
prefix_rule(pattern=["sed"], decision="allow")
prefix_rule(pattern=["cargo", "run", "--bin", "rye-worker"], decision="allow")
prefix_rule(pattern=["sqlite3"], decision="allow")
prefix_rule(pattern=["find"], decision="allow")
prefix_rule(pattern=["git", "add"], decision="allow")
prefix_rule(pattern=["git", "commit"], decision="allow")
prefix_rule(pattern=["docker", "build"], decision="allow")
prefix_rule(pattern=["docker", "run"], decision="allow")
实际使用中,这个文件会变得越来越乱。一次对数据库查询的批准,最终可能沉淀为一条跨多行的规则,里面带着连接字符串、SQL 和环境变量引用。下面是一个真实案例(已精简):
prefix_rule(pattern=["/usr/bin/zsh", "-lc",
"set -a; source .env; set +a; psql \"$DATABASE_URL\" -X
-v ON_ERROR_STOP=1 -c \"SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'rye_proxy'...\""],
decision="allow")
这条规则只靠一次点击就生成了,从此成为后续会话中允许执行的命令集的一部分。它引用了环境变量文件和数据库地址,而这一切唯一的记录,只是一个本地的纯文本文件。
权限系统不做的事
Claude Code 的权限系统对想快速推进的独立开发者来说足够好用:日常操作少了很多确认弹窗,危险动作依然会被拦截。但它解决不了下面这些团队层面的问题:
没有集中式的策略分发。 权限规则按用户、按项目各自存在每台机器上。组织层面没有任何机制,可以把「拒绝所有 docker run 命令」或「任何引用 .env 文件的 Bash 调用都必须先审批」这样的策略,推送到每位开发者的 Claude Code 安装中。
没有审批人留痕。 当一条规则出现在 settings.local.json 里时,你无法查证是谁批准的、何时批准的、当时响应的是哪次工具调用。文件里只有当前生效的规则集。
没有过期机制。 规则一旦添加就会一直存在,直到有人手动改动文件。没有限时审批,没有定期复查,权限也不会自动收窄。
跨工具没有统一格式。 Claude Code 使用 Bash(cargo:*) 这样的模式;Codex 使用 prefix_rule(pattern=["cargo"], decision="allow");Cursor 又是另一套格式。团队如果同时用多个 AI 编程助手,就要分别理解三套互不兼容的权限系统。
没有独立校验。 权限系统只记录它同意了什么,并不会独立确认工具调用实际做了什么。如果你放行了 Bash(npm run:*),而 npm run build 里的 postinstall 脚本借机窃取并外传环境变量——这一层,权限系统是看不到的。
失败开放的钩子:如果 PreToolUse 钩子超时或崩溃,对应的工具调用照样继续。这对开发效率来说是好事,但也意味着,除非助手之外还有别的机制强制它生效,基于钩子的策略只是建议性质的。
--dangerously-skip-permissions 标志
Claude Code 提供了 --dangerously-skip-permissions 标志,用来彻底关闭权限系统。这个名字是故意起得这么吓人的,官方文档也写明它只适用于 CI/CD 和自动化工作流——这些场景没法进行交互式批准。
当这个标志启用时,所有工具调用——文件读取、写入、shell 命令、抓取网页——都不会经过任何权限检查。剩下的唯一护栏只有模型自身的指令,以及宿主环境提供的那点沙箱隔离。
这个标志存在自有道理:CI 任务、批量重构或其他无人值守运行,确实没法跑去点确认。但代价太粗糙:你要么拿到交互式权限,要么完全没有权限。没有内建的中间地带——比如“不每次问人、但强制某套集中策略”的模式。
对团队的影响
对于个人开发者来说,Claude Code 的权限系统绰绰有余:它在做任何有风险的操作前先询问,记住你的偏好,日常操作又不碍事。
挑战出现在团队规模下:权限系统本身有用,但团队还需要在它之上再叠一层——集中策略分发、审批审计、跨工具规范化、对实际发生的操作做独立核查,以及在无人值守运行中仍然有效的强制机制。